@mmstack/primitives 22.7.0 → 22.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.
@@ -4089,7 +4089,7 @@ function isOpaque(value) {
4089
4089
  function isWritableSignal(value) {
4090
4090
  return isWritableSignal$2(value);
4091
4091
  }
4092
- function isRecord(value) {
4092
+ function isRecord$1(value) {
4093
4093
  if (value === null || typeof value !== 'object' || isOpaque(value))
4094
4094
  return false;
4095
4095
  const proto = Object.getPrototypeOf(value);
@@ -4105,7 +4105,7 @@ function isLeafValue(value, vivifyEnabled) {
4105
4105
  return !vivifyEnabled;
4106
4106
  if (isOpaque(value))
4107
4107
  return true; // opaque always wins — even arrays
4108
- return !Array.isArray(value) && !isRecord(value);
4108
+ return !Array.isArray(value) && !isRecord$1(value);
4109
4109
  }
4110
4110
  /**
4111
4111
  * @internal
@@ -4118,7 +4118,7 @@ function resolveVivify(sample, option) {
4118
4118
  return false;
4119
4119
  if (Array.isArray(sample))
4120
4120
  return 'array';
4121
- if (isRecord(sample))
4121
+ if (isRecord$1(sample))
4122
4122
  return 'object';
4123
4123
  return 'auto';
4124
4124
  }
@@ -4143,7 +4143,7 @@ function createFallbackOnChange(target, prop, vivifyFn, isMutableSource) {
4143
4143
  ? container
4144
4144
  : Array.isArray(container)
4145
4145
  ? container.slice()
4146
- : isRecord(container)
4146
+ : isRecord$1(container)
4147
4147
  ? { ...container }
4148
4148
  : container; // non-plain leaf (Date/class instance): legacy in-place attempt
4149
4149
  try {
@@ -4280,7 +4280,7 @@ const isPlainArray$1 = (v) => Array.isArray(v) && !isOpaque(v);
4280
4280
  function diffNode(prev, next, path, ops) {
4281
4281
  if (Object.is(prev, next))
4282
4282
  return;
4283
- if (isRecord(prev) && isRecord(next)) {
4283
+ if (isRecord$1(prev) && isRecord$1(next)) {
4284
4284
  for (const key of Object.keys(prev)) {
4285
4285
  if (!Object.hasOwn(next, key))
4286
4286
  ops.push({ kind: 'delete', path: [...path, key], prev: prev[key] });
@@ -4313,9 +4313,11 @@ function diffNode(prev, next, path, ops) {
4313
4313
  /** Immutably applies one op along its path, vivifying missing containers `'auto'`-style. */
4314
4314
  function applyAt(container, path, idx, op) {
4315
4315
  const seg = path[idx];
4316
+ if (seg === '__proto__')
4317
+ return container;
4316
4318
  const base = isPlainArray$1(container)
4317
4319
  ? container.slice()
4318
- : isRecord(container)
4320
+ : isRecord$1(container)
4319
4321
  ? { ...container }
4320
4322
  : typeof seg === 'number'
4321
4323
  ? []
@@ -4344,6 +4346,8 @@ function applyOps(root, ops) {
4344
4346
  const list = Array.isArray(ops) ? ops : ops.ops;
4345
4347
  let next = root;
4346
4348
  for (const op of list) {
4349
+ if (op.kind === 'clear')
4350
+ continue; // register retirement, never a value change
4347
4351
  if (op.path.length === 0) {
4348
4352
  if (op.kind === 'set')
4349
4353
  next = op.next;
@@ -4357,7 +4361,8 @@ function applyOps(root, ops) {
4357
4361
  * Pure reference-pruned structural diff of two roots into minimal ops (the emission core of
4358
4362
  * {@link opLog}, exported so code outside a log can produce a batch — e.g. diffing a scratch
4359
4363
  * draft against a replica's current value to route a write to its owner). Trusts the
4360
- * copy-on-write contract: an untouched subtree that kept its reference is skipped.
4364
+ * copy-on-write contract: an untouched subtree that kept its reference is skipped. Emits only
4365
+ * `set` and `delete`; `clear` is an emission-layer intent, never a diff product.
4361
4366
  */
4362
4367
  function diffOps(prev, next) {
4363
4368
  const ops = [];
@@ -4368,20 +4373,21 @@ function diffOps(prev, next) {
4368
4373
  * Inverts a batch for undo: reversed order, `set`↔its own inverse (an add — a `set` with no
4369
4374
  * `prev` — inverts to a `delete`; a `delete` inverts to a `set` restoring `prev`). Feed the
4370
4375
  * result to {@link OpLog.apply}. Requires the ops' `prev`s, which in-memory batches always
4371
- * carry a wire-serialized batch that stripped them is not invertible.
4376
+ * carry (a wire-serialized batch that stripped them is not invertible). A `clear` is skipped:
4377
+ * it never changed a value, so it has no independent inverse (the accompanying subtree `set`'s
4378
+ * `prev` subsumes restoration).
4372
4379
  */
4373
4380
  function invertBatch(batch) {
4374
4381
  const ops = Array.isArray(batch) ? batch : batch.ops;
4375
4382
  const inverted = [];
4376
4383
  for (let i = ops.length - 1; i >= 0; i--) {
4377
4384
  const op = ops[i];
4385
+ if (op.kind === 'clear')
4386
+ continue;
4378
4387
  if (op.kind === 'delete') {
4379
- inverted.push({
4380
- kind: 'set',
4381
- path: op.path,
4382
- next: op.prev,
4383
- prev: undefined,
4384
- });
4388
+ // no `prev`: the key is ABSENT once the delete applied, so this inverse is an add —
4389
+ // inverting it again yields the delete back (redo removes the key, not sets undefined)
4390
+ inverted.push({ kind: 'set', path: op.path, next: op.prev });
4385
4391
  continue;
4386
4392
  }
4387
4393
  if (!Object.hasOwn(op, 'prev')) {
@@ -4535,7 +4541,7 @@ function buildChildNode(target, prop, isMutableSource, options) {
4535
4541
  const value = untracked(target);
4536
4542
  const nodeVivify = resolveVivify(value, options.vivify);
4537
4543
  const vivifyFn = createVivify(nodeVivify);
4538
- const equalFn = isMutableSource && (isRecord(value) || Array.isArray(value))
4544
+ const equalFn = isMutableSource && (isRecord$1(value) || Array.isArray(value))
4539
4545
  ? mutableChildEqual
4540
4546
  : undefined;
4541
4547
  const computation = derived(target, {
@@ -4584,7 +4590,7 @@ function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...r
4584
4590
  const v = source();
4585
4591
  if (Array.isArray(v) && !isOpaque(v))
4586
4592
  return 'array';
4587
- if (isRecord(v))
4593
+ if (isRecord$1(v))
4588
4594
  return 'record';
4589
4595
  return 'primitive';
4590
4596
  }, /* @ts-ignore */
@@ -4633,7 +4639,7 @@ function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...r
4633
4639
  arr[len] = 'length';
4634
4640
  return arr;
4635
4641
  }
4636
- if (!isRecord(v))
4642
+ if (!isRecord$1(v))
4637
4643
  return [];
4638
4644
  return Reflect.ownKeys(v);
4639
4645
  },
@@ -4651,7 +4657,7 @@ function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...r
4651
4657
  return { enumerable: true, configurable: true };
4652
4658
  return;
4653
4659
  }
4654
- if (!isRecord(v) || !(prop in v))
4660
+ if (!isRecord$1(v) || !(prop in v))
4655
4661
  return;
4656
4662
  return { enumerable: true, configurable: true };
4657
4663
  },
@@ -4991,23 +4997,110 @@ function createHlcClock(now = Date.now) {
4991
4997
  };
4992
4998
  }
4993
4999
 
4994
- const OP_PROTO_VERSION = 1;
5000
+ /**
5001
+ * Wire protocol version. Version 2 ops carry `cites` + `epoch` (the dot-citation register);
5002
+ * envelopes from other versions are dropped loudly: an op without citations cannot be merged
5003
+ * soundly (it would supersede nothing and its siblings would accumulate forever), so versions
5004
+ * are never silently mixed.
5005
+ */
5006
+ const OP_PROTO_VERSION = 2;
4995
5007
  const CONFLICT_BRAND = '~mmstackConflict';
4996
5008
  function isConflicted(value) {
4997
5009
  return typeof value === 'object' && value !== null && CONFLICT_BRAND in value;
4998
5010
  }
5011
+ const hasControlChar = (s) => {
5012
+ for (let i = 0; i < s.length; i++)
5013
+ if (s.charCodeAt(i) < 0x20)
5014
+ return true;
5015
+ return false;
5016
+ };
5017
+ const isCleanId = (v) => typeof v === 'string' && v.length > 0 && !hasControlChar(v);
5018
+ const isFiniteHlc = (h) => !!h &&
5019
+ typeof h === 'object' &&
5020
+ Number.isFinite(h.p) &&
5021
+ Number.isFinite(h.l);
5022
+ /**
5023
+ * Deterministic, total well-formedness check for a received envelope. Returns a short reason
5024
+ * string when the envelope must be rejected WHOLE, or `null` when it is well-formed. It reads only
5025
+ * the envelope (no clock, no local state), so every replica accepts or rejects a given envelope
5026
+ * identically. This validates SHAPE, not authority: it closes malformed input (control characters
5027
+ * in an id or path segment that could forge a path-key separator, a non-integer version, an unknown
5028
+ * op kind, a negative epoch, forged cites, a root delete, two ops racing on one path). Authority and
5029
+ * access control stay at the relay; direct peer-to-peer rooms are trust-full for authority, so this
5030
+ * shape check is a peer's only line against a malformed neighbor.
5031
+ */
5032
+ function validateEnvelope(env) {
5033
+ if (!env || typeof env !== 'object')
5034
+ return 'envelope';
5035
+ if (!isCleanId(env.origin))
5036
+ return 'origin';
5037
+ if (!isCleanId(env.writer))
5038
+ return 'writer';
5039
+ if (!isFiniteHlc(env.hlc))
5040
+ return 'hlc';
5041
+ if (!Number.isInteger(env.version) || env.version <= 0)
5042
+ return 'version';
5043
+ if (!Array.isArray(env.ops))
5044
+ return 'ops';
5045
+ const seenPaths = new Set();
5046
+ for (const op of env.ops) {
5047
+ if (!op || typeof op !== 'object')
5048
+ return 'op';
5049
+ if (op.kind !== 'set' && op.kind !== 'delete' && op.kind !== 'clear')
5050
+ return 'kind';
5051
+ if (!Array.isArray(op.path))
5052
+ return 'path';
5053
+ for (const seg of op.path) {
5054
+ if (typeof seg === 'string' && hasControlChar(seg))
5055
+ return 'path-control';
5056
+ if (seg === '__proto__')
5057
+ return 'path-proto';
5058
+ }
5059
+ if (op.path.length === 0 && op.kind !== 'set')
5060
+ return 'root-op';
5061
+ const epoch = op.epoch;
5062
+ if (typeof epoch !== 'number' || !Number.isFinite(epoch) || epoch < 0)
5063
+ return 'epoch';
5064
+ const cites = op.cites;
5065
+ if (!Array.isArray(cites))
5066
+ return 'cites';
5067
+ for (const c of cites) {
5068
+ if (!c ||
5069
+ typeof c !== 'object' ||
5070
+ !isCleanId(c.origin) ||
5071
+ !isFiniteHlc(c.hlc)) {
5072
+ return 'cites';
5073
+ }
5074
+ }
5075
+ // one op per path per envelope: a dot is (origin, hlc), so two ops on one path in one envelope
5076
+ // would share a dot and break the register's per-origin bookkeeping. Segments with control
5077
+ // characters are already rejected above, so this join is unambiguous.
5078
+ const key = op.path.map(String).join(String.fromCharCode(0x1f));
5079
+ if (seenPaths.has(key))
5080
+ return 'dup-path';
5081
+ seenPaths.add(key);
5082
+ }
5083
+ return null;
5084
+ }
4999
5085
  const lww = (_ancestor, mine) => mine;
5000
5086
  const mergeThree = (ancestor, mine, theirs) => merge3(ancestor, mine, theirs);
5001
- const preserve = (ancestor, mine, theirs) => ({ [CONFLICT_BRAND]: true, mine, theirs, ancestor });
5087
+ const preserve = (ancestor, mine, theirs) => ({
5088
+ [CONFLICT_BRAND]: true,
5089
+ siblings: [mine, theirs],
5090
+ mine,
5091
+ theirs,
5092
+ ancestor,
5093
+ });
5002
5094
  /**
5003
5095
  * Identity-aware array merge: reconciles two concurrent versions of
5004
5096
  * an array item-wise by a user-provided identity, instead of last-writer-wins on the whole
5005
5097
  * array. Items are matched by key; per-item fields merge via `merge3` against the ancestor
5006
5098
  * item; items added on either side survive; an item removed on either side and unedited on
5007
5099
  * the other stays removed. Item ORDER follows `mine` (the total-order winner), with `theirs`-
5008
- * only additions appended positional merging is out of scope (fractional indexing is the
5009
- * known upgrade if dogfooding demands it). Arrays still TRAVEL as whole-value sets; identity
5010
- * only shapes conflict resolution, so the wire format is untouched.
5100
+ * only additions appended, and arrays still TRAVEL as whole-value sets. For a list whose elements
5101
+ * move and edit concurrently, model it as a keyed container (a record of elements ordered by
5102
+ * `posBetween`) instead: `insertElement`/`moveElement`/`removeElement` write per element, so a
5103
+ * reorder and a concurrent edit both survive and elements travel one at a time.
5011
5104
  */
5012
5105
  function keyedArray(identity, opt) {
5013
5106
  const mergeItem = opt?.item ?? mergeThree;
@@ -5055,19 +5148,23 @@ function compilePolicies(entries) {
5055
5148
  merge: e.merge,
5056
5149
  }));
5057
5150
  }
5151
+ function matchSegments(segments, path) {
5152
+ if (segments.length !== path.length)
5153
+ return false;
5154
+ for (let i = 0; i < path.length; i++) {
5155
+ if (segments[i] !== '*' && segments[i] !== String(path[i]))
5156
+ return false;
5157
+ }
5158
+ return true;
5159
+ }
5058
5160
  function policyFor(policies, path) {
5059
- outer: for (const p of policies) {
5060
- if (p.segments.length !== path.length)
5061
- continue;
5062
- for (let i = 0; i < path.length; i++) {
5063
- if (p.segments[i] !== '*' && p.segments[i] !== String(path[i]))
5064
- continue outer;
5065
- }
5066
- return p.merge;
5161
+ for (const p of policies) {
5162
+ if (matchSegments(p.segments, path))
5163
+ return p.merge;
5067
5164
  }
5068
5165
  return lww;
5069
5166
  }
5070
- const SEP = '';
5167
+ const SEP = ''; // unit separator: keeps joined path keys prefix-unambiguous
5071
5168
  const keyOf$1 = (path) => path.map(String).join(SEP);
5072
5169
  function structuralEq(a, b) {
5073
5170
  if (Object.is(a, b))
@@ -5092,108 +5189,539 @@ function structuralEq(a, b) {
5092
5189
  }
5093
5190
  return true;
5094
5191
  }
5095
- // total order (hlc, writer, origin): two origins can share a writer AND a stamp
5096
- // (independent clocks, same ms), so only origin makes the order strict
5097
- const compareStamp = (a, b) => {
5192
+ const kindClass = (k) => (k === 'clear' ? 0 : 1);
5193
+ /**
5194
+ * The register's total order: max by `(epoch, kind-class, hlc, writer, origin)`, where `set`
5195
+ * and `delete` outrank `clear` at equal epoch. Epoch first makes an authority bump decisive
5196
+ * regardless of clocks (and closes stale-value resurrection); the kind-class tier makes a
5197
+ * concurrent edit's survival of a subtree replace categorical rather than a clock race; origin
5198
+ * last keeps the order strict when two replicas share a writer and a stamp.
5199
+ */
5200
+ function compareSiblings(a, b) {
5201
+ if (a.epoch !== b.epoch)
5202
+ return a.epoch - b.epoch;
5203
+ const kc = kindClass(a.kind) - kindClass(b.kind);
5204
+ if (kc !== 0)
5205
+ return kc;
5098
5206
  const byTotal = compareTotal(a.hlc, a.writer, b.hlc, b.writer);
5099
5207
  if (byTotal !== 0)
5100
5208
  return byTotal;
5101
5209
  return a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0;
5210
+ }
5211
+ const maxSibling = (siblings) => siblings.reduce((a, b) => (compareSiblings(a, b) >= 0 ? a : b));
5212
+ /** Last-writer-wins over the live sibling set: the {@link compareSiblings} maximum, as-is. */
5213
+ const defaultFold = (siblings) => {
5214
+ const winner = maxSibling(siblings);
5215
+ return winner.kind === 'set'
5216
+ ? { kind: 'set', value: winner.value }
5217
+ : { kind: winner.kind };
5102
5218
  };
5103
- const beats = (a, b) => compareStamp(a, b) > 0;
5219
+ // preserve on the register seam: every top-precedence live sibling survives as data. A delete
5220
+ // competes as a value (it may surface inside the conflict as `undefined`); lower-epoch siblings
5221
+ // never surface (the epoch gate stays outermost).
5222
+ const preserveFold = (siblings) => {
5223
+ const winner = maxSibling(siblings);
5224
+ if (winner.kind === 'clear')
5225
+ return { kind: 'clear' };
5226
+ const top = siblings.filter((s) => s.epoch === winner.epoch && s.kind !== 'clear');
5227
+ if (top.length === 1) {
5228
+ return top[0].kind === 'set'
5229
+ ? { kind: 'set', value: top[0].value }
5230
+ : { kind: 'delete' };
5231
+ }
5232
+ const ordered = [...top].sort((a, b) => compareSiblings(b, a));
5233
+ const values = ordered.map((s) => (s.kind === 'set' ? s.value : undefined));
5234
+ const conflicted = {
5235
+ [CONFLICT_BRAND]: true,
5236
+ siblings: values,
5237
+ mine: values[0],
5238
+ theirs: values[1],
5239
+ ancestor: ordered[1].prev,
5240
+ };
5241
+ return { kind: 'set', value: conflicted };
5242
+ };
5243
+ // A two-sided MergeFn generalized to N siblings: reduce over the canonically-ordered
5244
+ // top-precedence set, winner first, each step merging the next sibling against its own `prev`
5245
+ // as the ancestor. The iteration order is a pure function of the set, so the result converges
5246
+ // even for merges that are not associative (the reason pairwise-at-arrival diverged).
5247
+ const mergeFold = (merge) => {
5248
+ return (siblings, ctx) => {
5249
+ const ordered = [...siblings].sort((a, b) => compareSiblings(b, a));
5250
+ const winner = ordered[0];
5251
+ if (winner.kind !== 'set')
5252
+ return { kind: winner.kind };
5253
+ let acc = winner.value;
5254
+ for (let i = 1; i < ordered.length; i++) {
5255
+ const s = ordered[i];
5256
+ if (s.kind !== 'set' || s.epoch !== winner.epoch)
5257
+ continue;
5258
+ acc = merge(s.prev, acc, s.value, ctx);
5259
+ }
5260
+ return { kind: 'set', value: acc };
5261
+ };
5262
+ };
5263
+ const isContainer = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
5104
5264
  /**
5105
- * The unsequenced-topology convergence core: a per-path last-writer-wins
5106
- * register map over the total order (hlc, writer), with subtree dominance. Order-independent:
5107
- * any arrival order of the same envelope set yields the same state.
5265
+ * The unsequenced-topology convergence core: a dot-citation multi-value register per path.
5266
+ * An op supersedes exactly the sibling dots it cites; uncited concurrent writes stay live; a
5267
+ * pluggable fold resolves the live set at read. Both the live set and any pure fold over it
5268
+ * are functions of the delivered op SET, so any arrival order of the same envelopes (split,
5269
+ * duplicated, cites-before-ops) yields the same state.
5108
5270
  */
5109
5271
  function createConvergingApply(opt) {
5110
5272
  const registers = new Map();
5273
+ // per-path floor of this replica's own emitted epochs: monotone, survives reset() so a
5274
+ // rehydrated replica can never re-emit below an epoch it already exposed
5275
+ const floors = new Map();
5276
+ // monotone ingest counter + the seq at which each live sibling arrived (keyed pathKey → origin).
5277
+ // captureFrontier() reads the counter in O(1); a frontier-scoped stamp cites only siblings at or
5278
+ // below the captured seq. Side-mapped so the public sibling/checkpoint shapes stay unchanged.
5279
+ let ingestSeq = 0;
5280
+ const seqs = new Map();
5281
+ const setSeq = (key, origin, seq) => {
5282
+ let sm = seqs.get(key);
5283
+ if (!sm)
5284
+ seqs.set(key, (sm = new Map()));
5285
+ sm.set(origin, seq);
5286
+ };
5111
5287
  const policies = compilePolicies(opt?.policies ?? []);
5112
- const resolveConcurrent = (winner, loser, path) => {
5288
+ const customFolds = (opt?.folds ?? []).map((e) => ({
5289
+ segments: typeof e.path === 'string' ? e.path.split('.') : e.path.map(String),
5290
+ fold: e.fold,
5291
+ }));
5292
+ const foldFor = (path) => {
5293
+ for (const f of customFolds) {
5294
+ if (matchSegments(f.segments, path))
5295
+ return f.fold;
5296
+ }
5113
5297
  const merge = policyFor(policies, path);
5114
- if (merge === lww || winner.kind === 'delete' || loser.kind === 'delete') {
5115
- return winner;
5298
+ if (merge === lww)
5299
+ return defaultFold;
5300
+ if (merge === preserve)
5301
+ return preserveFold;
5302
+ return mergeFold(merge);
5303
+ };
5304
+ const regAt = (path) => {
5305
+ const key = keyOf$1(path);
5306
+ let reg = registers.get(key);
5307
+ if (!reg) {
5308
+ reg = { path, siblings: new Map(), water: new Map(), sig: '' };
5309
+ registers.set(key, reg);
5310
+ }
5311
+ return reg;
5312
+ };
5313
+ const liveOf = (reg) => {
5314
+ const out = [];
5315
+ for (const [o, s] of reg.siblings) {
5316
+ const w = reg.water.get(o);
5317
+ if (!w || compareHlc(s.hlc, w) > 0)
5318
+ out.push(s);
5116
5319
  }
5117
- const resolved = merge(loser.prev, winner.next, loser.next, { path });
5118
- if (Object.is(resolved, winner.next))
5119
- return winner;
5120
- return { kind: 'set', path, next: resolved, prev: winner.next };
5320
+ return out.sort((a, b) => a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0);
5321
+ };
5322
+ // The live siblings a frontier had observed: those that arrived at or below its captured seq.
5323
+ // Used by a fork commit so it supersedes only what it saw when it forked, not later writes.
5324
+ const liveObserved = (reg, frontier) => {
5325
+ const live = liveOf(reg);
5326
+ if (!frontier)
5327
+ return live;
5328
+ const sm = seqs.get(keyOf$1(reg.path));
5329
+ return live.filter((s) => (sm?.get(s.origin) ?? 0) <= frontier.seq);
5121
5330
  };
5122
- const concurrentWith = (incoming, registered) => {
5123
- if (incoming.kind === 'delete' || registered.kind === 'delete')
5331
+ // JSON of a tuple array, not a separator-joined string: `origin` is a caller-supplied value on a
5332
+ // P2P peer, so a naive `origin@p.l#epoch` join lets a crafted origin collide the signatures of two
5333
+ // distinct live sets. A collision makes refresh() skip a fold update, and since that skip is
5334
+ // arrival-order-sensitive it breaks convergence. JSON.stringify escapes the strings and the array
5335
+ // structure is unambiguous, so the signature is injective in the live set.
5336
+ const sigOf = (live) => JSON.stringify(live.map((s) => [s.origin, s.hlc.p, s.hlc.l, s.epoch, s.kind]));
5337
+ /** Recompute the fold cache; true iff the materialized result meaningfully changed. */
5338
+ const refresh = (reg) => {
5339
+ const live = liveOf(reg);
5340
+ const sig = sigOf(live);
5341
+ if (sig === reg.sig)
5124
5342
  return false;
5125
- if (!Object.hasOwn(incoming, 'prev'))
5126
- return true;
5127
- return !structuralEq(incoming.prev, registered.next);
5343
+ reg.sig = sig;
5344
+ const next = live.length
5345
+ ? foldFor(reg.path)(live, { path: reg.path })
5346
+ : undefined;
5347
+ const prev = reg.result;
5348
+ const same = prev === next ||
5349
+ (!!prev &&
5350
+ !!next &&
5351
+ prev.kind === next.kind &&
5352
+ (prev.kind !== 'set' ||
5353
+ next.kind !== 'set' ||
5354
+ Object.is(prev.value, next.value) ||
5355
+ structuralEq(prev.value, next.value)));
5356
+ if (same)
5357
+ return false; // keep the previous result object: reference identity is the contract
5358
+ reg.result = next;
5359
+ return true;
5360
+ };
5361
+ const descendantsOf = (key) => {
5362
+ const out = [];
5363
+ for (const [k, r] of registers) {
5364
+ if (k === key)
5365
+ continue;
5366
+ if (key === '' ? k !== '' : k.startsWith(key + SEP))
5367
+ out.push(r);
5368
+ }
5369
+ return out.sort((a, b) => a.path.length - b.path.length ||
5370
+ (keyOf$1(a.path) < keyOf$1(b.path) ? -1 : 1));
5371
+ };
5372
+ /** Does `value` still hold a key at `rel` (present, not merely undefined)? */
5373
+ const holdsKey = (value, rel) => {
5374
+ let cur = value;
5375
+ for (const seg of rel) {
5376
+ if (cur === null ||
5377
+ typeof cur !== 'object' ||
5378
+ !Object.hasOwn(cur, String(seg))) {
5379
+ return false;
5380
+ }
5381
+ cur = cur[String(seg)];
5382
+ }
5383
+ return true;
5384
+ };
5385
+ // A lone tombstone is droppable only if nothing else still materializes its key: no live
5386
+ // descendant register would resurface, and no live ancestor `set` value still holds it. Mirrors
5387
+ // the relay's retention twin so a client that prunes converges with a joiner seeded from the relay.
5388
+ const tombstoneDroppable = (key, reg) => {
5389
+ for (const [k, other] of registers) {
5390
+ if (k === key)
5391
+ continue;
5392
+ if (k.startsWith(key + SEP)) {
5393
+ if (liveOf(other).length > 0)
5394
+ return false;
5395
+ }
5396
+ else if (key.startsWith(k === '' ? '' : k + SEP)) {
5397
+ const rel = reg.path.slice(other.path.length);
5398
+ for (const s of liveOf(other)) {
5399
+ if (s.kind === 'set' && holdsKey(s.value, rel))
5400
+ return false;
5401
+ }
5402
+ }
5403
+ }
5404
+ return true;
5405
+ };
5406
+ /** Nearest ancestor register that contributes a value or a deletion (clears abstain). */
5407
+ const nearestContributing = (path) => {
5408
+ for (let len = path.length - 1; len >= 0; len--) {
5409
+ const reg = registers.get(keyOf$1(path.slice(0, len)));
5410
+ if (reg?.result && reg.result.kind !== 'clear')
5411
+ return reg;
5412
+ }
5413
+ return undefined;
5414
+ };
5415
+ // graft with the deterministic type-change rule: a graft whose parent location is not a plain
5416
+ // record is DROPPED (the register stays intact and resurfaces if the container is restored)
5417
+ const graft = (tree, rel, res) => {
5418
+ if (!isContainer(tree))
5419
+ return tree;
5420
+ const head = String(rel[0]);
5421
+ if (rel.length === 1) {
5422
+ if (res.kind === 'delete') {
5423
+ if (!Object.hasOwn(tree, head))
5424
+ return tree;
5425
+ const copy = { ...tree };
5426
+ delete copy[head];
5427
+ return copy;
5428
+ }
5429
+ return { ...tree, [head]: res.value };
5430
+ }
5431
+ if (!Object.hasOwn(tree, head)) {
5432
+ // vivify an absent middle container so a checkpoint-seeded materialization matches a peer that
5433
+ // applied the ops incrementally (incremental apply creates missing parents). A numeric next
5434
+ // segment vivifies an array, else an object, mirroring the incremental apply path.
5435
+ const vivified = typeof rel[1] === 'number' ? [] : {};
5436
+ return { ...tree, [head]: graft(vivified, rel.slice(1), res) };
5437
+ }
5438
+ const child = graft(tree[head], rel.slice(1), res);
5439
+ return child === tree[head] ? tree : { ...tree, [head]: child };
5440
+ };
5441
+ /** Would a value at `rel` under `value` materialize, per the graft rules? */
5442
+ const graftable = (value, rel) => {
5443
+ let cur = value;
5444
+ for (let i = 0; i < rel.length - 1; i++) {
5445
+ if (!isContainer(cur) || !Object.hasOwn(cur, String(rel[i])))
5446
+ return false;
5447
+ cur = cur[String(rel[i])];
5448
+ }
5449
+ return isContainer(cur);
5450
+ };
5451
+ /**
5452
+ * Whether a value at `path` materializes: every contributing ancestor register down the
5453
+ * chain must be a `set` whose value composes containers to the next one. The drop rule is
5454
+ * checked against the WHOLE chain, since a graft fine under its nearest ancestor can still drop
5455
+ * at a scalar further up.
5456
+ */
5457
+ const shows = (path) => {
5458
+ let holder;
5459
+ for (let len = 0; len < path.length; len++) {
5460
+ const reg = registers.get(keyOf$1(path.slice(0, len)));
5461
+ if (!reg?.result || reg.result.kind === 'clear')
5462
+ continue;
5463
+ if (holder) {
5464
+ const hres = holder.result;
5465
+ if (!hres || hres.kind !== 'set')
5466
+ return false;
5467
+ if (!graftable(hres.value, reg.path.slice(holder.path.length))) {
5468
+ return false;
5469
+ }
5470
+ }
5471
+ holder = reg;
5472
+ }
5473
+ if (!holder)
5474
+ return true; // nothing above constrains → vivify semantics
5475
+ const hres = holder.result;
5476
+ if (!hres || hres.kind !== 'set')
5477
+ return false;
5478
+ return graftable(hres.value, path.slice(holder.path.length));
5479
+ };
5480
+ /** Deepest-live-wins subtree value: the register's fold value with every live descendant fold grafted on. */
5481
+ const materializeAt = (base) => {
5482
+ const res = base.result;
5483
+ let tree = res && res.kind === 'set' ? res.value : undefined;
5484
+ for (const d of descendantsOf(keyOf$1(base.path))) {
5485
+ const r = d.result;
5486
+ if (!r || r.kind === 'clear')
5487
+ continue;
5488
+ if (!shows(d.path))
5489
+ continue; // dropped under a deleted/scalar ancestor (matches applied deltas)
5490
+ tree = graft(tree, d.path.slice(base.path.length), r);
5491
+ }
5492
+ return tree;
5493
+ };
5494
+ const deltas = (changed) => {
5495
+ changed.sort((a, b) => a.reg.path.length - b.reg.path.length ||
5496
+ (keyOf$1(a.reg.path) < keyOf$1(b.reg.path) ? -1 : 1));
5497
+ const out = [];
5498
+ const regions = [];
5499
+ const covered = (key) => regions.some((r) => key === r || (r === '' ? true : key.startsWith(r + SEP)));
5500
+ for (const { reg, before } of changed) {
5501
+ const key = keyOf$1(reg.path);
5502
+ if (covered(key))
5503
+ continue;
5504
+ const res = reg.result;
5505
+ if (!res || res.kind === 'clear') {
5506
+ // the register now abstains: re-materialize the nearest ancestor region it cleared out of
5507
+ if (!reg.path.length)
5508
+ continue;
5509
+ const anc = nearestContributing(reg.path);
5510
+ const ares = anc?.result;
5511
+ if (!anc || !ares || ares.kind !== 'set' || !shows(anc.path))
5512
+ continue;
5513
+ out.push({ kind: 'set', path: anc.path, next: materializeAt(anc) });
5514
+ regions.push(keyOf$1(anc.path));
5515
+ continue;
5516
+ }
5517
+ if (!shows(reg.path))
5518
+ continue; // dropped by the type-change rule or a deleted parent
5519
+ if (res.kind === 'delete') {
5520
+ if (!reg.path.length)
5521
+ continue; // a root delete is meaningless
5522
+ out.push({
5523
+ kind: 'delete',
5524
+ path: reg.path,
5525
+ prev: before?.kind === 'set' ? before.value : undefined,
5526
+ });
5527
+ }
5528
+ else {
5529
+ out.push({ kind: 'set', path: reg.path, next: materializeAt(reg) });
5530
+ }
5531
+ regions.push(key);
5532
+ }
5533
+ return out;
5128
5534
  };
5129
5535
  return {
5130
5536
  ingest: (env, o) => {
5131
- const stamp = {
5132
- hlc: env.hlc,
5133
- writer: env.writer,
5134
- origin: env.origin,
5135
- };
5136
- const out = [];
5537
+ const touched = new Map();
5538
+ const seq = ++ingestSeq;
5137
5539
  for (const op of env.ops) {
5540
+ if (o?.frontier && compareHlc(env.hlc, o.frontier) <= 0)
5541
+ continue; // below the pruned horizon
5542
+ // a delete or clear at the root has no parent register to abstain to; it can only blank the
5543
+ // whole document, and materialize would then disagree with the delta path, so drop it
5544
+ if (!op.path.length && op.kind !== 'set')
5545
+ continue;
5546
+ const reg = regAt(op.path);
5138
5547
  const key = keyOf$1(op.path);
5139
- let dominated = false;
5140
- let exact;
5141
- for (let len = 0; len <= op.path.length; len++) {
5142
- const reg = registers.get(keyOf$1(op.path.slice(0, len)));
5143
- if (!reg)
5548
+ if (!touched.has(key))
5549
+ touched.set(key, { reg, before: reg.result });
5550
+ const sop = op;
5551
+ for (const c of sop.cites ?? []) {
5552
+ // a self-citation (the op citing its own dot) would born-dead the write; ignore it
5553
+ if (c.origin === env.origin && compareHlc(c.hlc, env.hlc) === 0)
5144
5554
  continue;
5145
- if (len === op.path.length)
5146
- exact = reg;
5147
- else if (beats(reg, stamp)) {
5148
- dominated = true;
5149
- break;
5150
- }
5555
+ const cur = reg.water.get(c.origin);
5556
+ if (!cur || compareHlc(c.hlc, cur) > 0)
5557
+ reg.water.set(c.origin, c.hlc);
5151
5558
  }
5152
- if (dominated)
5153
- continue;
5154
- if (exact && beats(exact, stamp)) {
5155
- if (concurrentWith(op, exact.op)) {
5156
- const resolved = resolveConcurrent(exact.op, op, op.path);
5157
- if (resolved !== exact.op) {
5158
- exact.op = resolved;
5159
- if (!o?.local)
5160
- out.push(resolved);
5161
- }
5559
+ const best = reg.siblings.get(env.origin);
5560
+ if (!best || compareHlc(env.hlc, best.hlc) > 0) {
5561
+ const sib = {
5562
+ kind: op.kind,
5563
+ writer: env.writer,
5564
+ origin: env.origin,
5565
+ hlc: env.hlc,
5566
+ epoch: sop.epoch ?? 0,
5567
+ };
5568
+ if (op.kind === 'set')
5569
+ sib.value = op.next;
5570
+ if (op.kind !== 'clear' && Object.hasOwn(op, 'prev')) {
5571
+ sib.prev = op.prev;
5162
5572
  }
5163
- continue;
5573
+ reg.siblings.set(env.origin, sib);
5574
+ setSeq(key, env.origin, seq);
5164
5575
  }
5165
- let accepted = op;
5166
- if (exact && concurrentWith(op, exact.op)) {
5167
- accepted = resolveConcurrent(op, exact.op, op.path);
5576
+ if (o?.local && (sop.epoch ?? 0) > 0) {
5577
+ floors.set(key, Math.max(floors.get(key) ?? 0, sop.epoch));
5168
5578
  }
5169
- const isDescendant = key === ''
5170
- ? (k) => k !== ''
5171
- : (k) => k.startsWith(key + SEP);
5172
- const replays = [];
5173
- for (const [k, reg] of registers) {
5174
- if (!isDescendant(k))
5579
+ }
5580
+ const changed = [];
5581
+ for (const c of touched.values()) {
5582
+ if (refresh(c.reg))
5583
+ changed.push(c);
5584
+ }
5585
+ if ((o?.local && !o?.reconcile) || !changed.length)
5586
+ return [];
5587
+ return deltas(changed);
5588
+ },
5589
+ stamp: (ops, o) => {
5590
+ const out = [];
5591
+ const bump = o?.bump ? 1 : 0;
5592
+ const frontier = o?.frontier;
5593
+ const epochFor = (key, live) => {
5594
+ let e = floors.get(key) ?? 0;
5595
+ for (const s of live)
5596
+ if (s.epoch > e)
5597
+ e = s.epoch;
5598
+ return e + bump;
5599
+ };
5600
+ for (const op of ops) {
5601
+ const key = keyOf$1(op.path);
5602
+ const reg = registers.get(key);
5603
+ const live = reg ? liveObserved(reg, frontier) : [];
5604
+ out.push({
5605
+ ...op,
5606
+ cites: live.map((s) => ({ origin: s.origin, hlc: s.hlc })),
5607
+ epoch: epochFor(key, live),
5608
+ });
5609
+ if (op.kind === 'clear')
5610
+ continue;
5611
+ for (const d of descendantsOf(key)) {
5612
+ if (d.result?.kind === 'clear')
5613
+ continue; // already abstaining
5614
+ const dlive = liveObserved(d, frontier);
5615
+ if (!dlive.length)
5175
5616
  continue;
5176
- if (beats(stamp, reg))
5177
- registers.delete(k);
5178
- else
5179
- replays.push(reg);
5617
+ out.push({
5618
+ kind: 'clear',
5619
+ path: d.path,
5620
+ cites: dlive.map((s) => ({ origin: s.origin, hlc: s.hlc })),
5621
+ epoch: epochFor(keyOf$1(d.path), dlive),
5622
+ });
5180
5623
  }
5181
- replays.sort(compareStamp);
5182
- registers.set(key, {
5183
- hlc: env.hlc,
5184
- writer: env.writer,
5185
- origin: env.origin,
5186
- op: accepted,
5624
+ }
5625
+ return out;
5626
+ },
5627
+ captureFrontier: () => ({ seq: ingestSeq }),
5628
+ liveAt: (path) => {
5629
+ const reg = registers.get(keyOf$1(path));
5630
+ return reg ? liveOf(reg) : [];
5631
+ },
5632
+ materialize: () => {
5633
+ const root = registers.get('');
5634
+ const res = root?.result;
5635
+ let tree = res && res.kind === 'set' ? res.value : undefined;
5636
+ for (const d of descendantsOf('')) {
5637
+ const r = d.result;
5638
+ if (!r || r.kind === 'clear')
5639
+ continue;
5640
+ if (!shows(d.path))
5641
+ continue; // dropped under a deleted/scalar ancestor (matches applied deltas)
5642
+ if (tree === undefined)
5643
+ tree = {}; // vivify: deeper registers materialize without a root write
5644
+ tree = graft(tree, d.path, r);
5645
+ }
5646
+ return tree;
5647
+ },
5648
+ checkpoint: () => {
5649
+ const out = [];
5650
+ for (const reg of registers.values()) {
5651
+ out.push({
5652
+ path: reg.path,
5653
+ siblings: [...reg.siblings.values()],
5654
+ water: Object.fromEntries(reg.water),
5187
5655
  });
5188
- if (!o?.local) {
5189
- out.push(accepted);
5190
- for (const r of replays)
5191
- out.push(r.op);
5192
- }
5193
5656
  }
5194
5657
  return out;
5195
5658
  },
5196
- reset: () => registers.clear(),
5659
+ load: (regs) => {
5660
+ const seq = ++ingestSeq;
5661
+ for (const r of regs) {
5662
+ const reg = regAt(r.path);
5663
+ const key = keyOf$1(r.path);
5664
+ for (const s of r.siblings) {
5665
+ const cur = reg.siblings.get(s.origin);
5666
+ if (!cur || compareHlc(s.hlc, cur.hlc) > 0) {
5667
+ reg.siblings.set(s.origin, s);
5668
+ setSeq(key, s.origin, seq);
5669
+ }
5670
+ }
5671
+ for (const [o, h] of Object.entries(r.water)) {
5672
+ const cur = reg.water.get(o);
5673
+ if (!cur || compareHlc(h, cur) > 0)
5674
+ reg.water.set(o, h);
5675
+ }
5676
+ if (opt?.origin) {
5677
+ const own = reg.siblings.get(opt.origin);
5678
+ if (own) {
5679
+ floors.set(key, Math.max(floors.get(key) ?? 0, own.epoch));
5680
+ }
5681
+ }
5682
+ refresh(reg);
5683
+ }
5684
+ },
5685
+ prune: (frontier) => {
5686
+ for (const [key, reg] of [...registers]) {
5687
+ const sm = seqs.get(key);
5688
+ for (const [o, s] of [...reg.siblings]) {
5689
+ const w = reg.water.get(o);
5690
+ if (compareHlc(s.hlc, frontier) <= 0 &&
5691
+ w &&
5692
+ compareHlc(s.hlc, w) <= 0) {
5693
+ reg.siblings.delete(o);
5694
+ sm?.delete(o);
5695
+ }
5696
+ }
5697
+ for (const [o, h] of [...reg.water]) {
5698
+ if (compareHlc(h, frontier) <= 0)
5699
+ reg.water.delete(o);
5700
+ }
5701
+ if (reg.siblings.size === 0 && reg.water.size === 0) {
5702
+ registers.delete(key);
5703
+ seqs.delete(key);
5704
+ floors.delete(key);
5705
+ }
5706
+ }
5707
+ const byDepth = [...registers.entries()].sort((a, b) => b[1].path.length - a[1].path.length);
5708
+ for (const [key, reg] of byDepth) {
5709
+ const live = liveOf(reg);
5710
+ if (live.length === 1 &&
5711
+ live[0].kind === 'delete' &&
5712
+ reg.siblings.size === 1 &&
5713
+ compareHlc(live[0].hlc, frontier) <= 0 &&
5714
+ tombstoneDroppable(key, reg)) {
5715
+ registers.delete(key);
5716
+ seqs.delete(key);
5717
+ floors.delete(key);
5718
+ }
5719
+ }
5720
+ },
5721
+ reset: () => {
5722
+ registers.clear();
5723
+ seqs.clear();
5724
+ },
5197
5725
  };
5198
5726
  }
5199
5727
  function getAtPath(root, path) {
@@ -5221,6 +5749,10 @@ function rebaseOps(root, pending, remote, policies) {
5221
5749
  for (const batch of pending) {
5222
5750
  const next = [];
5223
5751
  for (const op of batch) {
5752
+ if (op.kind === 'clear') {
5753
+ next.push(op); // a register intent, not a value change: passes through untouched
5754
+ continue;
5755
+ }
5224
5756
  const cur = getAtPath(base, op.path);
5225
5757
  if (op.kind === 'delete') {
5226
5758
  next.push({ kind: 'delete', path: op.path, prev: cur });
@@ -5259,20 +5791,30 @@ function generateOrigin() {
5259
5791
  }
5260
5792
  /**
5261
5793
  * Wires a copy-on-write signal (a `store` root) to the op protocol: local writes emit
5262
- * stamped envelopes, received envelopes fold in through the converging apply. The
5263
- * unsequenced-topology client core that `tabSync(store)` and P2P transports build on.
5794
+ * stamped envelopes (citing the sibling dots they observed), received envelopes fold in
5795
+ * through the converging register. The unsequenced-topology client core that
5796
+ * `tabSync(store)` and P2P transports build on.
5264
5797
  */
5265
5798
  const RECENT_LOCAL_CAP = 64;
5266
5799
  function opSync(source, opt) {
5267
5800
  const origin = opt.origin ?? generateOrigin();
5268
5801
  const clock = opt.clock ?? createHlcClock();
5269
- const conv = createConvergingApply({ policies: opt.policies });
5802
+ const conv = createConvergingApply({
5803
+ policies: opt.policies,
5804
+ folds: opt.folds,
5805
+ origin,
5806
+ });
5270
5807
  const subscribers = new Set();
5271
5808
  // per-origin high-watermark; `versions.get(origin)` IS the local emit counter, so a hydrate/restore
5272
5809
  // that raises our own watermark also advances the next mint — no separate counter to drift out of
5273
5810
  // sync and collide with a version acked before a reboot but dropped from a debounced outbox.
5274
5811
  const versions = new Map();
5275
5812
  const recentLocal = [];
5813
+ // highest stability frontier this peer has pruned to. A remote envelope at or below it is a settled
5814
+ // straggler (its state is compacted away); re-admitting one could resurrect a value below the
5815
+ // frontier, and per-origin version dedup cannot catch a FIRST-CONTACT straggler (no prior entry),
5816
+ // so the frontier is the admission gate that closes that hole on the receive path.
5817
+ let prunedFrontier;
5276
5818
  const resolvedInjector = opt.driver
5277
5819
  ? null
5278
5820
  : (opt.injector ?? inject(Injector));
@@ -5293,6 +5835,9 @@ function opSync(source, opt) {
5293
5835
  const canDefer = !opt.driver;
5294
5836
  const outbox = [];
5295
5837
  let receiving = false;
5838
+ let bumping = false;
5839
+ // set while a synced fork's commit is emitting: freezes emission cites to what the fork observed
5840
+ let scopeFrontier;
5296
5841
  // a signal the drain reaction tracks; bumping it schedules an outbox drain for the next tick
5297
5842
  const drainTick = signal(0, /* @ts-ignore */
5298
5843
  ...(ngDevMode ? [{ debugName: "drainTick" }] : /* istanbul ignore next */ []));
@@ -5306,6 +5851,8 @@ function opSync(source, opt) {
5306
5851
  notify(env);
5307
5852
  };
5308
5853
  const emitLocal = (ops) => {
5854
+ const frontier = scopeFrontier;
5855
+ const stamped = conv.stamp(ops, { bump: bumping, frontier });
5309
5856
  const nextVersion = (versions.get(origin) ?? 0) + 1;
5310
5857
  const env = {
5311
5858
  proto: OP_PROTO_VERSION,
@@ -5314,10 +5861,19 @@ function opSync(source, opt) {
5314
5861
  version: nextVersion,
5315
5862
  hlc: clock.next(),
5316
5863
  policyVersion: opt.policyVersion ?? 0,
5317
- ops,
5864
+ ops: stamped,
5318
5865
  };
5319
5866
  versions.set(origin, nextVersion);
5320
- conv.ingest(env, { local: true });
5867
+ if (frontier) {
5868
+ // a fork commit: its ops are concurrent siblings (they cite only the fork-time frontier), so
5869
+ // move the store to the fold winner rather than leaving the raw committed value in place
5870
+ const reconciled = conv.ingest(env, { local: true, reconcile: true });
5871
+ if (reconciled.length)
5872
+ log.apply(reconciled);
5873
+ }
5874
+ else {
5875
+ conv.ingest(env, { local: true });
5876
+ }
5321
5877
  recentLocal.push(env);
5322
5878
  if (recentLocal.length > RECENT_LOCAL_CAP)
5323
5879
  recentLocal.shift();
@@ -5349,10 +5905,25 @@ function opSync(source, opt) {
5349
5905
  return;
5350
5906
  if (env.proto !== OP_PROTO_VERSION) {
5351
5907
  if (isDevMode()) {
5352
- console.warn(`[@mmstack/primitives] dropped envelope with proto ${env.proto} (expected ${OP_PROTO_VERSION})`);
5908
+ console.warn(`[@mmstack/primitives] dropped envelope with proto ${env.proto} (expected ${OP_PROTO_VERSION}: ops must carry cites + epoch; emitters from another protocol version are rejected rather than silently mixed)`);
5909
+ }
5910
+ return;
5911
+ }
5912
+ const reason = validateEnvelope(env);
5913
+ if (reason !== null) {
5914
+ if (isDevMode()) {
5915
+ console.warn(`[@mmstack/primitives] dropped malformed envelope (${reason}) from origin ${String(env.origin)}`);
5353
5916
  }
5917
+ opt.onReject?.(env, reason);
5354
5918
  return;
5355
5919
  }
5920
+ // a settled straggler at or below the pruned stability frontier: reject it (its state is
5921
+ // compacted, re-admitting could resurrect a below-frontier value). All ops in an envelope share
5922
+ // its stamp, so the envelope hlc is the dot for every op. The live relay path never delivers a
5923
+ // below-frontier op (a lagging client gets a snapshot, not a delta), so this only fires on a
5924
+ // stray re-broadcast, e.g. over a P2P/multi-path topology.
5925
+ if (prunedFrontier && compareHlc(env.hlc, prunedFrontier) <= 0)
5926
+ return;
5356
5927
  const known = versions.get(env.origin);
5357
5928
  if (known !== undefined && env.version <= known)
5358
5929
  return; // duplicate/covered — idempotent
@@ -5362,14 +5933,9 @@ function opSync(source, opt) {
5362
5933
  versions.set(env.origin, env.version);
5363
5934
  receiving = true;
5364
5935
  try {
5365
- // Freeze pending local FIRST — stamped by a clock that has NOT yet observed this remote, so
5366
- // the local write keeps its causally-independent (original) stamp rather than being lifted
5367
- // above the remote and always winning. Emission is deferred to a tick via the outbox; this
5368
- // only registers + stamps.
5369
5936
  log.flush();
5370
5937
  clock.observe(env.hlc);
5371
5938
  const ops = conv.ingest(env);
5372
- // apply the converged result — a local write that LOST its path rolls back visibly here
5373
5939
  if (ops.length)
5374
5940
  log.apply(ops);
5375
5941
  }
@@ -5381,45 +5947,88 @@ function opSync(source, opt) {
5381
5947
  drainOutbox();
5382
5948
  log.flush();
5383
5949
  },
5950
+ override: (fn) => {
5951
+ log.flush(); // earlier pending writes emit un-bumped
5952
+ bumping = true;
5953
+ try {
5954
+ fn();
5955
+ log.flush();
5956
+ }
5957
+ finally {
5958
+ bumping = false;
5959
+ }
5960
+ },
5961
+ captureFrontier: () => {
5962
+ log.flush(); // fold pending base writes in first, so they count as observed
5963
+ return conv.captureFrontier();
5964
+ },
5965
+ commitScope: (frontier, fn) => {
5966
+ log.flush(); // earlier pending writes emit against the live frontier, not this one
5967
+ scopeFrontier = frontier;
5968
+ try {
5969
+ fn();
5970
+ log.flush(); // stamp + register the scoped writes now, while the frontier is frozen
5971
+ }
5972
+ finally {
5973
+ scopeFrontier = undefined;
5974
+ }
5975
+ },
5384
5976
  watermark: () => Object.fromEntries(versions),
5977
+ prune: (frontier) => {
5978
+ if (!prunedFrontier || compareHlc(frontier, prunedFrontier) > 0) {
5979
+ prunedFrontier = frontier;
5980
+ }
5981
+ conv.prune(frontier);
5982
+ },
5385
5983
  snapshot: () => {
5386
5984
  log.flush();
5387
- return { root: untracked(source), wm: Object.fromEntries(versions) };
5985
+ return {
5986
+ root: untracked(source),
5987
+ registers: conv.checkpoint(),
5988
+ wm: Object.fromEntries(versions),
5989
+ };
5388
5990
  },
5389
5991
  seed: () => {
5390
5992
  log.flush();
5391
5993
  emitLocal([{ kind: 'set', path: [], next: untracked(source) }]);
5392
5994
  },
5393
- hydrate: (root, wm) => {
5995
+ hydrate: (state, pending) => {
5394
5996
  log.flush();
5395
- const covered = wm?.[origin] ?? 0;
5396
- const pending = recentLocal.filter((e) => e.version > covered);
5997
+ // rebase this origin's uncovered local writes on top. A caller that keeps a durable outbox
5998
+ // (meshSync, the worker replica) passes its full unacked set, so a long offline burst larger
5999
+ // than the in-memory `recentLocal` cap is never dropped from the rebase; without it, fall back
6000
+ // to the recent-local ring.
6001
+ const source = pending ?? recentLocal;
6002
+ const toReplay = source.filter((e) => e.version > (state.wm?.[e.origin] ?? 0));
5397
6003
  conv.reset();
5398
- let next = root;
5399
- for (const e of pending)
5400
- next = applyOps(next, e.ops);
5401
- log.apply([{ kind: 'set', path: [], next }]);
5402
- for (const [o, v] of Object.entries(wm ?? {})) {
6004
+ conv.load(state.registers ?? []);
6005
+ const deltas = [];
6006
+ for (const e of toReplay) {
6007
+ deltas.push(...conv.ingest(e, { local: true, reconcile: true }));
6008
+ }
6009
+ log.apply([
6010
+ { kind: 'set', path: [], next: applyOps(state.root, deltas) },
6011
+ ]);
6012
+ for (const [o, v] of Object.entries(state.wm ?? {})) {
5403
6013
  versions.set(o, Math.max(versions.get(o) ?? 0, v));
5404
6014
  }
5405
- for (const e of pending)
5406
- conv.ingest(e, { local: true });
5407
6015
  },
5408
6016
  restore: (envs, highWater) => {
5409
- let maxV = versions.get(origin) ?? 0;
6017
+ let tailOrigin;
5410
6018
  for (const env of envs) {
5411
- if (env.origin !== origin)
5412
- continue; // only this origin's own durable outbox
5413
6019
  clock.observe(env.hlc); // keep the clock ≥ restored stamps before any future mint
5414
6020
  log.apply(env.ops); // reflect the offline edit in the store, echo-free
5415
6021
  conv.ingest(env, { local: true }); // register as a local winner (survives a reconnect merge)
5416
6022
  recentLocal.push(env);
5417
6023
  if (recentLocal.length > RECENT_LOCAL_CAP)
5418
6024
  recentLocal.shift();
5419
- maxV = Math.max(maxV, env.version);
6025
+ versions.set(env.origin, Math.max(versions.get(env.origin) ?? 0, env.version));
6026
+ tailOrigin = env.origin;
5420
6027
  notify(env); // hand to the transport to resend the unacknowledged tail
5421
6028
  }
5422
- versions.set(origin, Math.max(maxV, highWater ?? 0));
6029
+ if (highWater != null && tailOrigin != null) {
6030
+ versions.set(tailOrigin, Math.max(versions.get(tailOrigin) ?? 0, highWater));
6031
+ }
5423
6032
  },
5424
6033
  destroy: () => {
5425
6034
  drainOutbox(); // don't silently drop frozen-but-unsent local writes
@@ -5430,7 +6039,199 @@ function opSync(source, opt) {
5430
6039
  },
5431
6040
  };
5432
6041
  }
6042
+ /**
6043
+ * Fork a synced store for isolated edits (an agent branch, a staged review), keeping the correct
6044
+ * emission semantics on commit. The fork observes the base as it was when this call ran; committing
6045
+ * emits its diff citing only those observed dots, so an edit that landed on the base mid-flight
6046
+ * stays a concurrent sibling and the configured fold decides between them, rather than the commit
6047
+ * overwriting a write it never saw. `rebase()` re-observes the base (a following commit then
6048
+ * supersedes what is visible now, the reviewed-and-apply step). Pass the same `store` and `sync`
6049
+ * that are wired together; the fork is a plain {@link Fork} otherwise, so `forkStore` itself stays
6050
+ * sync-agnostic.
6051
+ */
6052
+ function syncedFork(sync, store, opt) {
6053
+ let frontier = sync.captureFrontier();
6054
+ const f = forkStore(store, opt);
6055
+ return {
6056
+ store: f.store,
6057
+ ops: f.ops,
6058
+ commit: () => sync.commitScope(frontier, () => f.commit()),
6059
+ discard: () => {
6060
+ f.discard();
6061
+ frontier = sync.captureFrontier();
6062
+ },
6063
+ rebase: () => {
6064
+ frontier = sync.captureFrontier();
6065
+ },
6066
+ };
6067
+ }
6068
+
6069
+ /**
6070
+ * Reserved key holding an element's fractional position inside a keyed container. It lives INSIDE
6071
+ * the element (at `[container, elementKey, '~pos']`), so a reorder is a one-field write that never
6072
+ * collides with a concurrent edit to the element's data. It stays visible on the materialized
6073
+ * element value; do not read, write, or strip it by hand, use the helpers in this file.
6074
+ */
6075
+ const POS_SEGMENT = '~pos';
6076
+ // Order-preserving fractional-index digits. The alphabet is ASCII-ascending, so a plain string
6077
+ // comparison of two positions matches their fractional order with no decoding.
6078
+ const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
6079
+ const BASE = DIGITS.length;
6080
+ const digitOf = (c) => DIGITS.indexOf(c);
6081
+ // Repeated inserts into the SAME gap grow a position one digit at a time. Healthy positions stay a
6082
+ // few characters; a long one signals a hot insertion point that should be rebalanced.
6083
+ const POS_WARN_LENGTH = 48;
6084
+ let posWarned = false;
6085
+ const warnIfLong = (pos) => {
6086
+ if (isDevMode() && !posWarned && pos.length >= POS_WARN_LENGTH) {
6087
+ posWarned = true;
6088
+ console.warn(`[@mmstack/primitives] a keyed-container position grew to ${pos.length} characters from ` +
6089
+ `repeated inserts into one gap. Call rebalanceContainer(...) to reclaim precision.`);
6090
+ }
6091
+ return pos;
6092
+ };
6093
+ /**
6094
+ * A compact position string strictly between `before` and `after`, ordered by plain string
6095
+ * comparison. Pass `undefined` for an open end: `posBetween()` seeds the first element,
6096
+ * `posBetween(last)` appends, `posBetween(undefined, first)` prepends. Repeated inserts into the
6097
+ * same gap grow the string one digit at a time rather than colliding, and the result is never equal
6098
+ * to either neighbor. `before` must sort before `after`.
6099
+ */
6100
+ function posBetween(before, after) {
6101
+ // Neighbors can tie (concurrent inserts into the same gap leave two equal positions, ordered only
6102
+ // by key). There is no position strictly between equal bounds, so open the upper end: the new
6103
+ // position sorts just after them and stays deterministic instead of looping.
6104
+ const upper = before != null && after != null && before >= after ? undefined : after;
6105
+ let i = 0;
6106
+ let out = '';
6107
+ // The upper bound only opens to BASE once we pass `after`'s last constraining digit: an adjacent
6108
+ // pair (gap of 1) leaves no room here, so we commit the lower digit and everything deeper is free.
6109
+ let upperOpen = upper == null;
6110
+ for (;;) {
6111
+ const lo = before != null && i < before.length ? digitOf(before[i]) : 0;
6112
+ const hi = upperOpen || upper == null ? BASE : i < upper.length ? digitOf(upper[i]) : 0;
6113
+ if (hi - lo >= 2)
6114
+ return warnIfLong(out + DIGITS[lo + ((hi - lo) >> 1)]);
6115
+ if (hi === lo) {
6116
+ // digits equal: no room yet, but `after` still constrains deeper digits, keep following it
6117
+ out += DIGITS[lo];
6118
+ i++;
6119
+ continue;
6120
+ }
6121
+ // gap of 1: commit the lower digit and open the upper bound (deeper digits only exceed `before`)
6122
+ out += DIGITS[lo];
6123
+ i++;
6124
+ upperOpen = true;
6125
+ }
6126
+ }
6127
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
6128
+ /**
6129
+ * A keyed container's elements in reading order. Order is a pure function of the materialized
6130
+ * value: elements sort by their `~pos` string, ties broken by key. An element whose `~pos` is
6131
+ * missing or not a string is ordered as if its position were the empty string (it sorts first,
6132
+ * key breaking the tie), so a peer that dropped the position field still lands somewhere
6133
+ * deterministic on every replica.
6134
+ */
6135
+ function orderedEntries(container) {
6136
+ const entries = [];
6137
+ for (const key of Object.keys(container)) {
6138
+ const value = container[key];
6139
+ const raw = isRecord(value) ? value[POS_SEGMENT] : undefined;
6140
+ entries.push({ key, pos: typeof raw === 'string' ? raw : '', value });
6141
+ }
6142
+ entries.sort((a, b) => a.pos < b.pos ? -1 : a.pos > b.pos ? 1 : a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
6143
+ return entries;
6144
+ }
6145
+ const devError = (msg) => {
6146
+ if (typeof ngDevMode !== 'undefined' && ngDevMode)
6147
+ console.error(`[keyed-container] ${msg}`);
6148
+ };
6149
+ const neighborPositions = (entries, index) => {
6150
+ const clamped = Math.max(0, Math.min(index, entries.length));
6151
+ return [entries[clamped - 1]?.pos || undefined, entries[clamped]?.pos || undefined];
6152
+ };
6153
+ /**
6154
+ * Insert `value` under `key` at `index` in reading order (default: append). The position is
6155
+ * computed from the neighbors at that index, so the element lands where asked without renumbering
6156
+ * any sibling. A keyed container is a RECORD, never an array, so this is a per-key write the sync
6157
+ * layer diffs on its own. Returns the assigned position. Re-inserting an existing key overwrites it.
6158
+ */
6159
+ function insertElement(container, key, value, index) {
6160
+ if (POS_SEGMENT in value)
6161
+ devError(`insertElement: '${POS_SEGMENT}' is managed, drop it from the value`);
6162
+ const entries = orderedEntries(container()).filter((e) => e.key !== key);
6163
+ const [before, after] = neighborPositions(entries, index ?? entries.length);
6164
+ const pos = posBetween(before, after);
6165
+ container.update((c) => ({ ...c, [key]: { ...value, [POS_SEGMENT]: pos } }));
6166
+ return pos;
6167
+ }
6168
+ /**
6169
+ * Move the element at `key` to `index` in reading order. This writes ONLY the element's `~pos`
6170
+ * field, so it never conflicts with a concurrent edit to the same element's data (they land on
6171
+ * different paths and both survive). Returns the new position, or `undefined` if `key` is absent.
6172
+ */
6173
+ function moveElement(container, key, index) {
6174
+ const current = container()[key];
6175
+ if (current == null)
6176
+ return undefined;
6177
+ const entries = orderedEntries(container()).filter((e) => e.key !== key);
6178
+ const [before, after] = neighborPositions(entries, index);
6179
+ const pos = posBetween(before, after);
6180
+ container.update((c) => ({ ...c, [key]: { ...c[key], [POS_SEGMENT]: pos } }));
6181
+ return pos;
6182
+ }
6183
+ /** Remove the element at `key`. Deletes the whole element (a per-key delete the sync layer folds). */
6184
+ function removeElement(container, key) {
6185
+ container.update((c) => {
6186
+ if (!(key in c))
6187
+ return c;
6188
+ const next = { ...c };
6189
+ delete next[key];
6190
+ return next;
6191
+ });
6192
+ }
6193
+ /**
6194
+ * Reassign every element's position to a fresh, evenly spaced sequence, as an authority write:
6195
+ * each `~pos` set is epoch-bumped so it wins the merge against any concurrent move, while leaving
6196
+ * concurrent edits to element DATA untouched (only the `~pos` fields are written). Use this to
6197
+ * reclaim precision after many same-gap inserts. Existing reading order is preserved.
6198
+ */
6199
+ function rebalanceContainer(sync, container) {
6200
+ const order = orderedEntries(container());
6201
+ const positions = evenPositions(order.length);
6202
+ sync.override(() => {
6203
+ container.update((c) => {
6204
+ const next = { ...c };
6205
+ order.forEach(({ key }, i) => {
6206
+ next[key] = { ...c[key], [POS_SEGMENT]: positions[i] };
6207
+ });
6208
+ return next;
6209
+ });
6210
+ });
6211
+ }
6212
+ // `n` evenly spaced, order-preserving positions in (0, 1): fraction (i+1)/(n+1) encoded to enough
6213
+ // base-62 digits that consecutive fractions never collide. Compact, so precision is reclaimed.
6214
+ function evenPositions(n) {
6215
+ if (n === 0)
6216
+ return [];
6217
+ const digits = Math.floor(Math.log(n + 1) / Math.log(BASE)) + 2;
6218
+ const out = [];
6219
+ for (let i = 0; i < n; i++) {
6220
+ let f = (i + 1) / (n + 1);
6221
+ let s = '';
6222
+ for (let k = 0; k < digits; k++) {
6223
+ f *= BASE;
6224
+ const d = Math.min(BASE - 1, Math.floor(f));
6225
+ s += DIGITS[d];
6226
+ f -= d;
6227
+ }
6228
+ out.push(s);
6229
+ }
6230
+ return out;
6231
+ }
5433
6232
 
6233
+ const PATH_SEP = '';
6234
+ const OP_SEP = '';
5434
6235
  /**
5435
6236
  * Undo/redo for a copy-on-write store, built on the op-log: each tracked change is stored as
5436
6237
  * its inverse batch, so `undo()` is one `apply` and history costs only the diffs, not full
@@ -5439,21 +6240,54 @@ function opSync(source, opt) {
5439
6240
  *
5440
6241
  * Composes with sync for collaborative undo: pass `track: syncClient` so only YOUR writes are
5441
6242
  * undoable, while `undo()` emits a normal op that propagates to peers (it writes through the
5442
- * store, which the sync client picks up).
6243
+ * store, which the sync client picks up). Coalescing groups only this stack's entries — what
6244
+ * goes over the wire is untouched.
5443
6245
  */
5444
6246
  function storeHistory(source, opt) {
5445
6247
  const limit = opt?.limit ?? 100;
6248
+ const coalesce = opt?.coalesce;
6249
+ const now = opt?.now ?? Date.now;
5446
6250
  const logOpt = { origin: opt?.origin };
5447
6251
  if (opt?.driver)
5448
6252
  logOpt.driver = opt.driver;
5449
6253
  else
5450
- logOpt.injector = opt?.injector ?? inject(Injector);
6254
+ logOpt.injector =
6255
+ opt?.injector ?? inject(Injector);
5451
6256
  const log = opLog(source, logOpt);
5452
6257
  const undoStack = [];
5453
6258
  const redoStack = [];
5454
6259
  const version = signal(0, /* @ts-ignore */
5455
6260
  ...(ngDevMode ? [{ debugName: "version" }] : /* istanbul ignore next */ [])); // monotonic: bumps on every mutation so the computeds recompute
5456
6261
  let applying = false;
6262
+ let runOpen = false;
6263
+ let lastAt = 0;
6264
+ let lastSig = '';
6265
+ const sigOf = (ops) => ops.map((o) => `${o.kind}:${o.path.join(PATH_SEP)}`).join(OP_SEP);
6266
+ const mergeInto = (entry, incoming) => {
6267
+ const merged = [...entry];
6268
+ const rest = [];
6269
+ for (const inc of incoming) {
6270
+ const key = inc.path.join(PATH_SEP);
6271
+ const at = merged.findIndex((o) => o.path.join(PATH_SEP) === key);
6272
+ const cur = at >= 0 ? merged[at] : undefined;
6273
+ if (cur && cur.kind === 'set' && inc.kind === 'set') {
6274
+ const composed = {
6275
+ kind: 'set',
6276
+ path: cur.path,
6277
+ next: cur.next,
6278
+ };
6279
+ // absent `prev` means the composed inverse is an add (inverts to a delete): the newest
6280
+ // forward op removed the key, so redo must remove it again
6281
+ if (Object.hasOwn(inc, 'prev'))
6282
+ composed.prev = inc.prev;
6283
+ merged[at] = composed;
6284
+ }
6285
+ else {
6286
+ rest.push(inc);
6287
+ }
6288
+ }
6289
+ return rest.length ? [...rest, ...merged] : merged;
6290
+ };
5457
6291
  const push = (stack, inverse) => {
5458
6292
  stack.push(inverse);
5459
6293
  if (stack.length > limit)
@@ -5464,20 +6298,39 @@ function storeHistory(source, opt) {
5464
6298
  return; // an undo/redo's own emission must not re-enter history
5465
6299
  if (!batch.ops.length)
5466
6300
  return;
5467
- push(undoStack, invertBatch(batch));
6301
+ const inverse = invertBatch(batch);
6302
+ const at = now();
6303
+ const sig = coalesce ? sigOf(batch.ops) : '';
6304
+ if (coalesce &&
6305
+ runOpen &&
6306
+ undoStack.length > 0 &&
6307
+ at - lastAt <= coalesce.ms &&
6308
+ (coalesce.samePath === false || sig === lastSig)) {
6309
+ undoStack[undoStack.length - 1] = mergeInto(undoStack[undoStack.length - 1], inverse);
6310
+ }
6311
+ else {
6312
+ push(undoStack, inverse);
6313
+ }
6314
+ runOpen = true;
6315
+ lastAt = at;
6316
+ lastSig = sig;
5468
6317
  redoStack.length = 0; // a fresh edit forks the timeline
5469
6318
  version.update((v) => v + 1);
5470
6319
  };
5471
6320
  // track the sync client's local stream when given, else self-diff every store change
5472
6321
  const unsub = (opt?.track ?? log).subscribe(record);
6322
+ const flushTrack = () => opt?.track?.flush?.();
5473
6323
  const run = (from, to) => {
6324
+ flushTrack();
6325
+ log.flush();
5474
6326
  const inverse = from.pop();
5475
6327
  if (!inverse)
5476
6328
  return;
5477
- log.flush(); // settle pending local writes before applying
6329
+ runOpen = false; // stepping through history is a boundary: the next edit starts fresh
5478
6330
  applying = true;
5479
6331
  try {
5480
6332
  log.apply(inverse);
6333
+ flushTrack();
5481
6334
  }
5482
6335
  finally {
5483
6336
  applying = false;
@@ -5490,7 +6343,11 @@ function storeHistory(source, opt) {
5490
6343
  canRedo: computed(() => (version(), redoStack.length > 0)),
5491
6344
  undo: () => run(undoStack, redoStack),
5492
6345
  redo: () => run(redoStack, undoStack),
6346
+ checkpoint: () => {
6347
+ runOpen = false;
6348
+ },
5493
6349
  clear: () => {
6350
+ runOpen = false;
5494
6351
  undoStack.length = 0;
5495
6352
  redoStack.length = 0;
5496
6353
  version.update((v) => v + 1);
@@ -5702,7 +6559,7 @@ const isPlainArray = (v) => Array.isArray(v) && !isOpaque(v);
5702
6559
  function keyOf(item, key) {
5703
6560
  if (typeof key === 'function')
5704
6561
  return key(item);
5705
- return isRecord(item) ? item[key] : item;
6562
+ return isRecord$1(item) ? item[key] : item;
5706
6563
  }
5707
6564
  /**
5708
6565
  * Produces a value equal to `next` but sharing as much of `prev`'s reference structure as possible:
@@ -5731,7 +6588,7 @@ function reconcileValue(prev, next, key) {
5731
6588
  });
5732
6589
  return changed ? out : prev;
5733
6590
  }
5734
- if (isRecord(prev) && isRecord(next)) {
6591
+ if (isRecord$1(prev) && isRecord$1(next)) {
5735
6592
  const nextKeys = Object.keys(next);
5736
6593
  let changed = Object.keys(prev).length !== nextKeys.length;
5737
6594
  const out = {};
@@ -6017,7 +6874,7 @@ function storeTabSync(sig, opt, bus, injector) {
6017
6874
  const covered = Object.entries(snap.wm).every(([origin, v]) => (msg.wm[origin] ?? 0) >= v);
6018
6875
  post(covered
6019
6876
  ? { t: 'uptodate', to: msg.from }
6020
- : { t: 'state', to: msg.from, root: snap.root, wm: snap.wm });
6877
+ : { t: 'state', to: msg.from, state: snap });
6021
6878
  }, Math.random() * jitterMs);
6022
6879
  responseTimers.set(msg.from, timer);
6023
6880
  return;
@@ -6032,7 +6889,7 @@ function storeTabSync(sig, opt, bus, injector) {
6032
6889
  if (msg.to !== sync.origin || phase !== 'joining')
6033
6890
  return;
6034
6891
  if (msg.t === 'state')
6035
- sync.hydrate(msg.root, msg.wm);
6892
+ sync.hydrate(msg.state);
6036
6893
  goLive();
6037
6894
  return;
6038
6895
  }
@@ -6409,5 +7266,5 @@ function withHistory(sourceOrValue, opt) {
6409
7266
  * Generated bundle index. Do not edit.
6410
7267
  */
6411
7268
 
6412
- 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 };
7269
+ 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 };
6413
7270
  //# sourceMappingURL=mmstack-primitives.mjs.map