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