@mmstack/primitives 20.11.1 → 20.13.0

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.
@@ -4121,7 +4121,7 @@ function isOpaque(value) {
4121
4121
  value[OPAQUE] === true);
4122
4122
  }
4123
4123
 
4124
- function isRecord(value) {
4124
+ function isRecord$1(value) {
4125
4125
  if (value === null || typeof value !== 'object' || isOpaque(value))
4126
4126
  return false;
4127
4127
  const proto = Object.getPrototypeOf(value);
@@ -4137,7 +4137,7 @@ function isLeafValue(value, vivifyEnabled) {
4137
4137
  return !vivifyEnabled;
4138
4138
  if (isOpaque(value))
4139
4139
  return true; // opaque always wins — even arrays
4140
- return !Array.isArray(value) && !isRecord(value);
4140
+ return !Array.isArray(value) && !isRecord$1(value);
4141
4141
  }
4142
4142
  /**
4143
4143
  * @internal
@@ -4150,7 +4150,7 @@ function resolveVivify(sample, option) {
4150
4150
  return false;
4151
4151
  if (Array.isArray(sample))
4152
4152
  return 'array';
4153
- if (isRecord(sample))
4153
+ if (isRecord$1(sample))
4154
4154
  return 'object';
4155
4155
  return 'auto';
4156
4156
  }
@@ -4175,7 +4175,7 @@ function createFallbackOnChange(target, prop, vivifyFn, isMutableSource) {
4175
4175
  ? container
4176
4176
  : Array.isArray(container)
4177
4177
  ? container.slice()
4178
- : isRecord(container)
4178
+ : isRecord$1(container)
4179
4179
  ? { ...container }
4180
4180
  : container; // non-plain leaf (Date/class instance): legacy in-place attempt
4181
4181
  try {
@@ -4206,7 +4206,7 @@ const isPlainArray$1 = (v) => Array.isArray(v) && !isOpaque(v);
4206
4206
  function diffNode(prev, next, path, ops) {
4207
4207
  if (Object.is(prev, next))
4208
4208
  return;
4209
- if (isRecord(prev) && isRecord(next)) {
4209
+ if (isRecord$1(prev) && isRecord$1(next)) {
4210
4210
  for (const key of Object.keys(prev)) {
4211
4211
  if (!Object.hasOwn(next, key))
4212
4212
  ops.push({ kind: 'delete', path: [...path, key], prev: prev[key] });
@@ -4239,9 +4239,11 @@ function diffNode(prev, next, path, ops) {
4239
4239
  /** Immutably applies one op along its path, vivifying missing containers `'auto'`-style. */
4240
4240
  function applyAt(container, path, idx, op) {
4241
4241
  const seg = path[idx];
4242
+ if (seg === '__proto__')
4243
+ return container;
4242
4244
  const base = isPlainArray$1(container)
4243
4245
  ? container.slice()
4244
- : isRecord(container)
4246
+ : isRecord$1(container)
4245
4247
  ? { ...container }
4246
4248
  : typeof seg === 'number'
4247
4249
  ? []
@@ -4270,6 +4272,8 @@ function applyOps(root, ops) {
4270
4272
  const list = Array.isArray(ops) ? ops : ops.ops;
4271
4273
  let next = root;
4272
4274
  for (const op of list) {
4275
+ if (op.kind === 'clear')
4276
+ continue; // register retirement, never a value change
4273
4277
  if (op.path.length === 0) {
4274
4278
  if (op.kind === 'set')
4275
4279
  next = op.next;
@@ -4283,7 +4287,8 @@ function applyOps(root, ops) {
4283
4287
  * Pure reference-pruned structural diff of two roots into minimal ops (the emission core of
4284
4288
  * {@link opLog}, exported so code outside a log can produce a batch — e.g. diffing a scratch
4285
4289
  * draft against a replica's current value to route a write to its owner). Trusts the
4286
- * copy-on-write contract: an untouched subtree that kept its reference is skipped.
4290
+ * copy-on-write contract: an untouched subtree that kept its reference is skipped. Emits only
4291
+ * `set` and `delete`; `clear` is an emission-layer intent, never a diff product.
4287
4292
  */
4288
4293
  function diffOps(prev, next) {
4289
4294
  const ops = [];
@@ -4294,13 +4299,17 @@ function diffOps(prev, next) {
4294
4299
  * Inverts a batch for undo: reversed order, `set`↔its own inverse (an add — a `set` with no
4295
4300
  * `prev` — inverts to a `delete`; a `delete` inverts to a `set` restoring `prev`). Feed the
4296
4301
  * result to {@link OpLog.apply}. Requires the ops' `prev`s, which in-memory batches always
4297
- * carry a wire-serialized batch that stripped them is not invertible.
4302
+ * carry (a wire-serialized batch that stripped them is not invertible). A `clear` is skipped:
4303
+ * it never changed a value, so it has no independent inverse (the accompanying subtree `set`'s
4304
+ * `prev` subsumes restoration).
4298
4305
  */
4299
4306
  function invertBatch(batch) {
4300
4307
  const ops = Array.isArray(batch) ? batch : batch.ops;
4301
4308
  const inverted = [];
4302
4309
  for (let i = ops.length - 1; i >= 0; i--) {
4303
4310
  const op = ops[i];
4311
+ if (op.kind === 'clear')
4312
+ continue;
4304
4313
  if (op.kind === 'delete') {
4305
4314
  inverted.push({
4306
4315
  kind: 'set',
@@ -4524,7 +4533,7 @@ function buildChildNode(target, prop, isMutableSource, options) {
4524
4533
  const value = untracked(target);
4525
4534
  const nodeVivify = resolveVivify(value, options.vivify);
4526
4535
  const vivifyFn = createVivify(nodeVivify);
4527
- const equalFn = isMutableSource && (isRecord(value) || Array.isArray(value))
4536
+ const equalFn = isMutableSource && (isRecord$1(value) || Array.isArray(value))
4528
4537
  ? mutableChildEqual
4529
4538
  : undefined;
4530
4539
  const computation = derived(target, {
@@ -4573,7 +4582,7 @@ function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...r
4573
4582
  const v = source();
4574
4583
  if (Array.isArray(v) && !isOpaque(v))
4575
4584
  return 'array';
4576
- if (isRecord(v))
4585
+ if (isRecord$1(v))
4577
4586
  return 'record';
4578
4587
  return 'primitive';
4579
4588
  }, ...(ngDevMode ? [{ debugName: "kind" }] : []));
@@ -4621,7 +4630,7 @@ function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...r
4621
4630
  arr[len] = 'length';
4622
4631
  return arr;
4623
4632
  }
4624
- if (!isRecord(v))
4633
+ if (!isRecord$1(v))
4625
4634
  return [];
4626
4635
  return Reflect.ownKeys(v);
4627
4636
  },
@@ -4639,7 +4648,7 @@ function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...r
4639
4648
  return { enumerable: true, configurable: true };
4640
4649
  return;
4641
4650
  }
4642
- if (!isRecord(v) || !(prop in v))
4651
+ if (!isRecord$1(v) || !(prop in v))
4643
4652
  return;
4644
4653
  return { enumerable: true, configurable: true };
4645
4654
  },
@@ -4981,23 +4990,274 @@ function createHlcClock(now = Date.now) {
4981
4990
  };
4982
4991
  }
4983
4992
 
4984
- const OP_PROTO_VERSION = 1;
4993
+ /**
4994
+ * Reserved key holding an element's fractional position inside a keyed container. It lives INSIDE
4995
+ * the element (at `[container, elementKey, '~pos']`), so a reorder is a one-field write that never
4996
+ * collides with a concurrent edit to the element's data. It stays visible on the materialized
4997
+ * element value; do not read, write, or strip it by hand, use the helpers in this file.
4998
+ */
4999
+ const POS_SEGMENT = '~pos';
5000
+ // Order-preserving fractional-index digits. The alphabet is ASCII-ascending, so a plain string
5001
+ // comparison of two positions matches their fractional order with no decoding.
5002
+ const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
5003
+ const BASE = DIGITS.length;
5004
+ const digitOf = (c) => DIGITS.indexOf(c);
5005
+ // Repeated inserts into the SAME gap grow a position one digit at a time. Healthy positions stay a
5006
+ // few characters; a long one signals a hot insertion point that should be rebalanced.
5007
+ const POS_WARN_LENGTH = 48;
5008
+ let posWarned = false;
5009
+ const warnIfLong = (pos) => {
5010
+ if (isDevMode() && !posWarned && pos.length >= POS_WARN_LENGTH) {
5011
+ posWarned = true;
5012
+ console.warn(`[@mmstack/primitives] a keyed-container position grew to ${pos.length} characters from ` +
5013
+ `repeated inserts into one gap. Call rebalanceContainer(...) to reclaim precision.`);
5014
+ }
5015
+ return pos;
5016
+ };
5017
+ /**
5018
+ * A compact position string strictly between `before` and `after`, ordered by plain string
5019
+ * comparison. Pass `undefined` for an open end: `posBetween()` seeds the first element,
5020
+ * `posBetween(last)` appends, `posBetween(undefined, first)` prepends. Repeated inserts into the
5021
+ * same gap grow the string one digit at a time rather than colliding, and the result is never equal
5022
+ * to either neighbor. `before` must sort before `after`.
5023
+ */
5024
+ function posBetween(before, after) {
5025
+ // Neighbors can tie (concurrent inserts into the same gap leave two equal positions, ordered only
5026
+ // by key). There is no position strictly between equal bounds, so open the upper end: the new
5027
+ // position sorts just after them and stays deterministic instead of looping.
5028
+ const upper = before != null && after != null && before >= after ? undefined : after;
5029
+ let i = 0;
5030
+ let out = '';
5031
+ // The upper bound only opens to BASE once we pass `after`'s last constraining digit: an adjacent
5032
+ // pair (gap of 1) leaves no room here, so we commit the lower digit and everything deeper is free.
5033
+ let upperOpen = upper == null;
5034
+ for (;;) {
5035
+ const lo = before != null && i < before.length ? digitOf(before[i]) : 0;
5036
+ const hi = upperOpen || upper == null ? BASE : i < upper.length ? digitOf(upper[i]) : 0;
5037
+ if (hi - lo >= 2)
5038
+ return warnIfLong(out + DIGITS[lo + ((hi - lo) >> 1)]);
5039
+ if (hi === lo) {
5040
+ // digits equal: no room yet, but `after` still constrains deeper digits, keep following it
5041
+ out += DIGITS[lo];
5042
+ i++;
5043
+ continue;
5044
+ }
5045
+ // gap of 1: commit the lower digit and open the upper bound (deeper digits only exceed `before`)
5046
+ out += DIGITS[lo];
5047
+ i++;
5048
+ upperOpen = true;
5049
+ }
5050
+ }
5051
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
5052
+ /**
5053
+ * A keyed container's elements in reading order. Order is a pure function of the materialized
5054
+ * value: elements sort by their `~pos` string, ties broken by key. An element whose `~pos` is
5055
+ * missing or not a string is ordered as if its position were the empty string (it sorts first,
5056
+ * key breaking the tie), so a peer that dropped the position field still lands somewhere
5057
+ * deterministic on every replica.
5058
+ */
5059
+ function orderedEntries(container) {
5060
+ const entries = [];
5061
+ for (const key of Object.keys(container)) {
5062
+ const value = container[key];
5063
+ const raw = isRecord(value) ? value[POS_SEGMENT] : undefined;
5064
+ entries.push({ key, pos: typeof raw === 'string' ? raw : '', value });
5065
+ }
5066
+ 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);
5067
+ return entries;
5068
+ }
5069
+ const devError = (msg) => {
5070
+ if (typeof ngDevMode !== 'undefined' && ngDevMode)
5071
+ console.error(`[keyed-container] ${msg}`);
5072
+ };
5073
+ const neighborPositions = (entries, index) => {
5074
+ const clamped = Math.max(0, Math.min(index, entries.length));
5075
+ return [entries[clamped - 1]?.pos || undefined, entries[clamped]?.pos || undefined];
5076
+ };
5077
+ /**
5078
+ * Insert `value` under `key` at `index` in reading order (default: append). The position is
5079
+ * computed from the neighbors at that index, so the element lands where asked without renumbering
5080
+ * any sibling. A keyed container is a RECORD, never an array, so this is a per-key write the sync
5081
+ * layer diffs on its own. Returns the assigned position. Re-inserting an existing key overwrites it.
5082
+ */
5083
+ function insertElement(container, key, value, index) {
5084
+ if (POS_SEGMENT in value)
5085
+ devError(`insertElement: '${POS_SEGMENT}' is managed, drop it from the value`);
5086
+ const entries = orderedEntries(container()).filter((e) => e.key !== key);
5087
+ const [before, after] = neighborPositions(entries, index ?? entries.length);
5088
+ const pos = posBetween(before, after);
5089
+ container.update((c) => ({ ...c, [key]: { ...value, [POS_SEGMENT]: pos } }));
5090
+ return pos;
5091
+ }
5092
+ /**
5093
+ * Move the element at `key` to `index` in reading order. This writes ONLY the element's `~pos`
5094
+ * field, so it never conflicts with a concurrent edit to the same element's data (they land on
5095
+ * different paths and both survive). Returns the new position, or `undefined` if `key` is absent.
5096
+ */
5097
+ function moveElement(container, key, index) {
5098
+ const current = container()[key];
5099
+ if (current == null)
5100
+ return undefined;
5101
+ const entries = orderedEntries(container()).filter((e) => e.key !== key);
5102
+ const [before, after] = neighborPositions(entries, index);
5103
+ const pos = posBetween(before, after);
5104
+ container.update((c) => ({ ...c, [key]: { ...c[key], [POS_SEGMENT]: pos } }));
5105
+ return pos;
5106
+ }
5107
+ /** Remove the element at `key`. Deletes the whole element (a per-key delete the sync layer folds). */
5108
+ function removeElement(container, key) {
5109
+ container.update((c) => {
5110
+ if (!(key in c))
5111
+ return c;
5112
+ const next = { ...c };
5113
+ delete next[key];
5114
+ return next;
5115
+ });
5116
+ }
5117
+ /**
5118
+ * Reassign every element's position to a fresh, evenly spaced sequence, as an authority write:
5119
+ * each `~pos` set is epoch-bumped so it wins the merge against any concurrent move, while leaving
5120
+ * concurrent edits to element DATA untouched (only the `~pos` fields are written). Use this to
5121
+ * reclaim precision after many same-gap inserts. Existing reading order is preserved.
5122
+ */
5123
+ function rebalanceContainer(sync, container) {
5124
+ const order = orderedEntries(container());
5125
+ const positions = evenPositions(order.length);
5126
+ sync.override(() => {
5127
+ container.update((c) => {
5128
+ const next = { ...c };
5129
+ order.forEach(({ key }, i) => {
5130
+ next[key] = { ...c[key], [POS_SEGMENT]: positions[i] };
5131
+ });
5132
+ return next;
5133
+ });
5134
+ });
5135
+ }
5136
+ // `n` evenly spaced, order-preserving positions in (0, 1): fraction (i+1)/(n+1) encoded to enough
5137
+ // base-62 digits that consecutive fractions never collide. Compact, so precision is reclaimed.
5138
+ function evenPositions(n) {
5139
+ if (n === 0)
5140
+ return [];
5141
+ const digits = Math.floor(Math.log(n + 1) / Math.log(BASE)) + 2;
5142
+ const out = [];
5143
+ for (let i = 0; i < n; i++) {
5144
+ let f = (i + 1) / (n + 1);
5145
+ let s = '';
5146
+ for (let k = 0; k < digits; k++) {
5147
+ f *= BASE;
5148
+ const d = Math.min(BASE - 1, Math.floor(f));
5149
+ s += DIGITS[d];
5150
+ f -= d;
5151
+ }
5152
+ out.push(s);
5153
+ }
5154
+ return out;
5155
+ }
5156
+
5157
+ /**
5158
+ * Wire protocol version. Version 2 ops carry `cites` + `epoch` (the dot-citation register);
5159
+ * envelopes from other versions are dropped loudly: an op without citations cannot be merged
5160
+ * soundly (it would supersede nothing and its siblings would accumulate forever), so versions
5161
+ * are never silently mixed.
5162
+ */
5163
+ const OP_PROTO_VERSION = 2;
4985
5164
  const CONFLICT_BRAND = '~mmstackConflict';
4986
5165
  function isConflicted(value) {
4987
5166
  return typeof value === 'object' && value !== null && CONFLICT_BRAND in value;
4988
5167
  }
5168
+ const hasControlChar = (s) => {
5169
+ for (let i = 0; i < s.length; i++)
5170
+ if (s.charCodeAt(i) < 0x20)
5171
+ return true;
5172
+ return false;
5173
+ };
5174
+ const isCleanId = (v) => typeof v === 'string' && v.length > 0 && !hasControlChar(v);
5175
+ const isFiniteHlc = (h) => !!h &&
5176
+ typeof h === 'object' &&
5177
+ Number.isFinite(h.p) &&
5178
+ Number.isFinite(h.l);
5179
+ /**
5180
+ * Deterministic, total well-formedness check for a received envelope. Returns a short reason
5181
+ * string when the envelope must be rejected WHOLE, or `null` when it is well-formed. It reads only
5182
+ * the envelope (no clock, no local state), so every replica accepts or rejects a given envelope
5183
+ * identically. This validates SHAPE, not authority: it closes malformed input (control characters
5184
+ * in an id or path segment that could forge a path-key separator, a non-integer version, an unknown
5185
+ * op kind, a negative epoch, forged cites, a root delete, two ops racing on one path). Authority and
5186
+ * access control stay at the relay; direct peer-to-peer rooms are trust-full for authority, so this
5187
+ * shape check is a peer's only line against a malformed neighbor.
5188
+ */
5189
+ function validateEnvelope(env) {
5190
+ if (!env || typeof env !== 'object')
5191
+ return 'envelope';
5192
+ if (!isCleanId(env.origin))
5193
+ return 'origin';
5194
+ if (!isCleanId(env.writer))
5195
+ return 'writer';
5196
+ if (!isFiniteHlc(env.hlc))
5197
+ return 'hlc';
5198
+ if (!Number.isInteger(env.version) || env.version <= 0)
5199
+ return 'version';
5200
+ if (!Array.isArray(env.ops))
5201
+ return 'ops';
5202
+ const seenPaths = new Set();
5203
+ for (const op of env.ops) {
5204
+ if (!op || typeof op !== 'object')
5205
+ return 'op';
5206
+ if (op.kind !== 'set' && op.kind !== 'delete' && op.kind !== 'clear')
5207
+ return 'kind';
5208
+ if (!Array.isArray(op.path))
5209
+ return 'path';
5210
+ for (const seg of op.path) {
5211
+ if (typeof seg === 'string' && hasControlChar(seg))
5212
+ return 'path-control';
5213
+ if (seg === '__proto__')
5214
+ return 'path-proto';
5215
+ }
5216
+ if (op.path.length === 0 && op.kind !== 'set')
5217
+ return 'root-op';
5218
+ const epoch = op.epoch;
5219
+ if (typeof epoch !== 'number' || !Number.isFinite(epoch) || epoch < 0)
5220
+ return 'epoch';
5221
+ const cites = op.cites;
5222
+ if (!Array.isArray(cites))
5223
+ return 'cites';
5224
+ for (const c of cites) {
5225
+ if (!c ||
5226
+ typeof c !== 'object' ||
5227
+ !isCleanId(c.origin) ||
5228
+ !isFiniteHlc(c.hlc)) {
5229
+ return 'cites';
5230
+ }
5231
+ }
5232
+ // one op per path per envelope: a dot is (origin, hlc), so two ops on one path in one envelope
5233
+ // would share a dot and break the register's per-origin bookkeeping. Segments with control
5234
+ // characters are already rejected above, so this join is unambiguous.
5235
+ const key = op.path.map(String).join(String.fromCharCode(0x1f));
5236
+ if (seenPaths.has(key))
5237
+ return 'dup-path';
5238
+ seenPaths.add(key);
5239
+ }
5240
+ return null;
5241
+ }
4989
5242
  const lww = (_ancestor, mine) => mine;
4990
5243
  const mergeThree = (ancestor, mine, theirs) => merge3(ancestor, mine, theirs);
4991
- const preserve = (ancestor, mine, theirs) => ({ [CONFLICT_BRAND]: true, mine, theirs, ancestor });
5244
+ const preserve = (ancestor, mine, theirs) => ({
5245
+ [CONFLICT_BRAND]: true,
5246
+ siblings: [mine, theirs],
5247
+ mine,
5248
+ theirs,
5249
+ ancestor,
5250
+ });
4992
5251
  /**
4993
- * Identity-aware array merge (op-protocol RFC §12 v0): reconciles two concurrent versions of
5252
+ * Identity-aware array merge: reconciles two concurrent versions of
4994
5253
  * an array item-wise by a user-provided identity, instead of last-writer-wins on the whole
4995
5254
  * array. Items are matched by key; per-item fields merge via `merge3` against the ancestor
4996
5255
  * item; items added on either side survive; an item removed on either side and unedited on
4997
5256
  * the other stays removed. Item ORDER follows `mine` (the total-order winner), with `theirs`-
4998
- * only additions appended positional merging is out of scope (fractional indexing is the
4999
- * known upgrade if dogfooding demands it). Arrays still TRAVEL as whole-value sets; identity
5000
- * only shapes conflict resolution, so the wire format is untouched.
5257
+ * only additions appended, and arrays still TRAVEL as whole-value sets. For a list whose elements
5258
+ * move and edit concurrently, model it as a keyed container (a record of elements ordered by
5259
+ * `posBetween`) instead: `insertElement`/`moveElement`/`removeElement` write per element, so a
5260
+ * reorder and a concurrent edit both survive and elements travel one at a time.
5001
5261
  */
5002
5262
  function keyedArray(identity, opt) {
5003
5263
  const mergeItem = opt?.item ?? mergeThree;
@@ -5021,9 +5281,7 @@ function keyedArray(identity, opt) {
5021
5281
  const other = theirsMap.get(key);
5022
5282
  const base = ancMap.get(key);
5023
5283
  if (theirsMap.has(key)) {
5024
- out.push(structuralEq(item, other)
5025
- ? item
5026
- : mergeItem(base, item, other, ctx));
5284
+ out.push(structuralEq(item, other) ? item : mergeItem(base, item, other, ctx));
5027
5285
  }
5028
5286
  else if (!ancMap.has(key) || !structuralEq(item, base)) {
5029
5287
  out.push(item); // added by mine, or edited by mine while theirs removed it → keep
@@ -5047,19 +5305,23 @@ function compilePolicies(entries) {
5047
5305
  merge: e.merge,
5048
5306
  }));
5049
5307
  }
5308
+ function matchSegments(segments, path) {
5309
+ if (segments.length !== path.length)
5310
+ return false;
5311
+ for (let i = 0; i < path.length; i++) {
5312
+ if (segments[i] !== '*' && segments[i] !== String(path[i]))
5313
+ return false;
5314
+ }
5315
+ return true;
5316
+ }
5050
5317
  function policyFor(policies, path) {
5051
- outer: for (const p of policies) {
5052
- if (p.segments.length !== path.length)
5053
- continue;
5054
- for (let i = 0; i < path.length; i++) {
5055
- if (p.segments[i] !== '*' && p.segments[i] !== String(path[i]))
5056
- continue outer;
5057
- }
5058
- return p.merge;
5318
+ for (const p of policies) {
5319
+ if (matchSegments(p.segments, path))
5320
+ return p.merge;
5059
5321
  }
5060
5322
  return lww;
5061
5323
  }
5062
- const SEP = '';
5324
+ const SEP = ''; // unit separator: keeps joined path keys prefix-unambiguous
5063
5325
  const keyOf$1 = (path) => path.map(String).join(SEP);
5064
5326
  function structuralEq(a, b) {
5065
5327
  if (Object.is(a, b))
@@ -5084,102 +5346,539 @@ function structuralEq(a, b) {
5084
5346
  }
5085
5347
  return true;
5086
5348
  }
5087
- // total order (hlc, writer, origin): two origins can share a writer AND a stamp
5088
- // (independent clocks, same ms), so only origin makes the order strict
5089
- const compareStamp = (a, b) => {
5349
+ const kindClass = (k) => (k === 'clear' ? 0 : 1);
5350
+ /**
5351
+ * The register's total order: max by `(epoch, kind-class, hlc, writer, origin)`, where `set`
5352
+ * and `delete` outrank `clear` at equal epoch. Epoch first makes an authority bump decisive
5353
+ * regardless of clocks (and closes stale-value resurrection); the kind-class tier makes a
5354
+ * concurrent edit's survival of a subtree replace categorical rather than a clock race; origin
5355
+ * last keeps the order strict when two replicas share a writer and a stamp.
5356
+ */
5357
+ function compareSiblings(a, b) {
5358
+ if (a.epoch !== b.epoch)
5359
+ return a.epoch - b.epoch;
5360
+ const kc = kindClass(a.kind) - kindClass(b.kind);
5361
+ if (kc !== 0)
5362
+ return kc;
5090
5363
  const byTotal = compareTotal(a.hlc, a.writer, b.hlc, b.writer);
5091
5364
  if (byTotal !== 0)
5092
5365
  return byTotal;
5093
5366
  return a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0;
5367
+ }
5368
+ const maxSibling = (siblings) => siblings.reduce((a, b) => (compareSiblings(a, b) >= 0 ? a : b));
5369
+ /** Last-writer-wins over the live sibling set: the {@link compareSiblings} maximum, as-is. */
5370
+ const defaultFold = (siblings) => {
5371
+ const winner = maxSibling(siblings);
5372
+ return winner.kind === 'set'
5373
+ ? { kind: 'set', value: winner.value }
5374
+ : { kind: winner.kind };
5094
5375
  };
5095
- const beats = (a, b) => compareStamp(a, b) > 0;
5376
+ // preserve on the register seam: every top-precedence live sibling survives as data. A delete
5377
+ // competes as a value (it may surface inside the conflict as `undefined`); lower-epoch siblings
5378
+ // never surface (the epoch gate stays outermost).
5379
+ const preserveFold = (siblings) => {
5380
+ const winner = maxSibling(siblings);
5381
+ if (winner.kind === 'clear')
5382
+ return { kind: 'clear' };
5383
+ const top = siblings.filter((s) => s.epoch === winner.epoch && s.kind !== 'clear');
5384
+ if (top.length === 1) {
5385
+ return top[0].kind === 'set'
5386
+ ? { kind: 'set', value: top[0].value }
5387
+ : { kind: 'delete' };
5388
+ }
5389
+ const ordered = [...top].sort((a, b) => compareSiblings(b, a));
5390
+ const values = ordered.map((s) => (s.kind === 'set' ? s.value : undefined));
5391
+ const conflicted = {
5392
+ [CONFLICT_BRAND]: true,
5393
+ siblings: values,
5394
+ mine: values[0],
5395
+ theirs: values[1],
5396
+ ancestor: ordered[1].prev,
5397
+ };
5398
+ return { kind: 'set', value: conflicted };
5399
+ };
5400
+ // A two-sided MergeFn generalized to N siblings: reduce over the canonically-ordered
5401
+ // top-precedence set, winner first, each step merging the next sibling against its own `prev`
5402
+ // as the ancestor. The iteration order is a pure function of the set, so the result converges
5403
+ // even for merges that are not associative (the reason pairwise-at-arrival diverged).
5404
+ const mergeFold = (merge) => {
5405
+ return (siblings, ctx) => {
5406
+ const ordered = [...siblings].sort((a, b) => compareSiblings(b, a));
5407
+ const winner = ordered[0];
5408
+ if (winner.kind !== 'set')
5409
+ return { kind: winner.kind };
5410
+ let acc = winner.value;
5411
+ for (let i = 1; i < ordered.length; i++) {
5412
+ const s = ordered[i];
5413
+ if (s.kind !== 'set' || s.epoch !== winner.epoch)
5414
+ continue;
5415
+ acc = merge(s.prev, acc, s.value, ctx);
5416
+ }
5417
+ return { kind: 'set', value: acc };
5418
+ };
5419
+ };
5420
+ const isContainer = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
5096
5421
  /**
5097
- * The unsequenced-topology convergence core (op-protocol RFC §4): a per-path last-writer-wins
5098
- * register map over the total order (hlc, writer), with subtree dominance. Order-independent:
5099
- * any arrival order of the same envelope set yields the same state.
5422
+ * The unsequenced-topology convergence core: a dot-citation multi-value register per path.
5423
+ * An op supersedes exactly the sibling dots it cites; uncited concurrent writes stay live; a
5424
+ * pluggable fold resolves the live set at read. Both the live set and any pure fold over it
5425
+ * are functions of the delivered op SET, so any arrival order of the same envelopes (split,
5426
+ * duplicated, cites-before-ops) yields the same state.
5100
5427
  */
5101
5428
  function createConvergingApply(opt) {
5102
5429
  const registers = new Map();
5430
+ // per-path floor of this replica's own emitted epochs: monotone, survives reset() so a
5431
+ // rehydrated replica can never re-emit below an epoch it already exposed
5432
+ const floors = new Map();
5433
+ // monotone ingest counter + the seq at which each live sibling arrived (keyed pathKey → origin).
5434
+ // captureFrontier() reads the counter in O(1); a frontier-scoped stamp cites only siblings at or
5435
+ // below the captured seq. Side-mapped so the public sibling/checkpoint shapes stay unchanged.
5436
+ let ingestSeq = 0;
5437
+ const seqs = new Map();
5438
+ const setSeq = (key, origin, seq) => {
5439
+ let sm = seqs.get(key);
5440
+ if (!sm)
5441
+ seqs.set(key, (sm = new Map()));
5442
+ sm.set(origin, seq);
5443
+ };
5103
5444
  const policies = compilePolicies(opt?.policies ?? []);
5104
- const resolveConcurrent = (winner, loser, path) => {
5445
+ const customFolds = (opt?.folds ?? []).map((e) => ({
5446
+ segments: typeof e.path === 'string' ? e.path.split('.') : e.path.map(String),
5447
+ fold: e.fold,
5448
+ }));
5449
+ const foldFor = (path) => {
5450
+ for (const f of customFolds) {
5451
+ if (matchSegments(f.segments, path))
5452
+ return f.fold;
5453
+ }
5105
5454
  const merge = policyFor(policies, path);
5106
- if (merge === lww || winner.kind === 'delete' || loser.kind === 'delete') {
5107
- return winner;
5455
+ if (merge === lww)
5456
+ return defaultFold;
5457
+ if (merge === preserve)
5458
+ return preserveFold;
5459
+ return mergeFold(merge);
5460
+ };
5461
+ const regAt = (path) => {
5462
+ const key = keyOf$1(path);
5463
+ let reg = registers.get(key);
5464
+ if (!reg) {
5465
+ reg = { path, siblings: new Map(), water: new Map(), sig: '' };
5466
+ registers.set(key, reg);
5467
+ }
5468
+ return reg;
5469
+ };
5470
+ const liveOf = (reg) => {
5471
+ const out = [];
5472
+ for (const [o, s] of reg.siblings) {
5473
+ const w = reg.water.get(o);
5474
+ if (!w || compareHlc(s.hlc, w) > 0)
5475
+ out.push(s);
5108
5476
  }
5109
- const resolved = merge(loser.prev, winner.next, loser.next, { path });
5110
- if (Object.is(resolved, winner.next))
5111
- return winner;
5112
- return { kind: 'set', path, next: resolved, prev: winner.next };
5477
+ return out.sort((a, b) => a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0);
5113
5478
  };
5114
- // a sequential edit carries the value it overwrote; a mismatch means neither saw the other.
5115
- // Structural, not referential: identity never survives the wire, so a peer that built on
5116
- // the replicated copy of a value must still count as sequential.
5117
- const concurrentWith = (incoming, registered) => {
5118
- if (incoming.kind === 'delete' || registered.kind === 'delete')
5479
+ // The live siblings a frontier had observed: those that arrived at or below its captured seq.
5480
+ // Used by a fork commit so it supersedes only what it saw when it forked, not later writes.
5481
+ const liveObserved = (reg, frontier) => {
5482
+ const live = liveOf(reg);
5483
+ if (!frontier)
5484
+ return live;
5485
+ const sm = seqs.get(keyOf$1(reg.path));
5486
+ return live.filter((s) => (sm?.get(s.origin) ?? 0) <= frontier.seq);
5487
+ };
5488
+ // JSON of a tuple array, not a separator-joined string: `origin` is a caller-supplied value on a
5489
+ // P2P peer, so a naive `origin@p.l#epoch` join lets a crafted origin collide the signatures of two
5490
+ // distinct live sets. A collision makes refresh() skip a fold update, and since that skip is
5491
+ // arrival-order-sensitive it breaks convergence. JSON.stringify escapes the strings and the array
5492
+ // structure is unambiguous, so the signature is injective in the live set.
5493
+ const sigOf = (live) => JSON.stringify(live.map((s) => [s.origin, s.hlc.p, s.hlc.l, s.epoch, s.kind]));
5494
+ /** Recompute the fold cache; true iff the materialized result meaningfully changed. */
5495
+ const refresh = (reg) => {
5496
+ const live = liveOf(reg);
5497
+ const sig = sigOf(live);
5498
+ if (sig === reg.sig)
5119
5499
  return false;
5120
- if (!Object.hasOwn(incoming, 'prev'))
5121
- return true;
5122
- return !structuralEq(incoming.prev, registered.next);
5500
+ reg.sig = sig;
5501
+ const next = live.length
5502
+ ? foldFor(reg.path)(live, { path: reg.path })
5503
+ : undefined;
5504
+ const prev = reg.result;
5505
+ const same = prev === next ||
5506
+ (!!prev &&
5507
+ !!next &&
5508
+ prev.kind === next.kind &&
5509
+ (prev.kind !== 'set' ||
5510
+ next.kind !== 'set' ||
5511
+ Object.is(prev.value, next.value) ||
5512
+ structuralEq(prev.value, next.value)));
5513
+ if (same)
5514
+ return false; // keep the previous result object: reference identity is the contract
5515
+ reg.result = next;
5516
+ return true;
5517
+ };
5518
+ const descendantsOf = (key) => {
5519
+ const out = [];
5520
+ for (const [k, r] of registers) {
5521
+ if (k === key)
5522
+ continue;
5523
+ if (key === '' ? k !== '' : k.startsWith(key + SEP))
5524
+ out.push(r);
5525
+ }
5526
+ return out.sort((a, b) => a.path.length - b.path.length ||
5527
+ (keyOf$1(a.path) < keyOf$1(b.path) ? -1 : 1));
5528
+ };
5529
+ /** Does `value` still hold a key at `rel` (present, not merely undefined)? */
5530
+ const holdsKey = (value, rel) => {
5531
+ let cur = value;
5532
+ for (const seg of rel) {
5533
+ if (cur === null ||
5534
+ typeof cur !== 'object' ||
5535
+ !Object.hasOwn(cur, String(seg))) {
5536
+ return false;
5537
+ }
5538
+ cur = cur[String(seg)];
5539
+ }
5540
+ return true;
5541
+ };
5542
+ // A lone tombstone is droppable only if nothing else still materializes its key: no live
5543
+ // descendant register would resurface, and no live ancestor `set` value still holds it. Mirrors
5544
+ // the relay's retention twin so a client that prunes converges with a joiner seeded from the relay.
5545
+ const tombstoneDroppable = (key, reg) => {
5546
+ for (const [k, other] of registers) {
5547
+ if (k === key)
5548
+ continue;
5549
+ if (k.startsWith(key + SEP)) {
5550
+ if (liveOf(other).length > 0)
5551
+ return false;
5552
+ }
5553
+ else if (key.startsWith(k === '' ? '' : k + SEP)) {
5554
+ const rel = reg.path.slice(other.path.length);
5555
+ for (const s of liveOf(other)) {
5556
+ if (s.kind === 'set' && holdsKey(s.value, rel))
5557
+ return false;
5558
+ }
5559
+ }
5560
+ }
5561
+ return true;
5562
+ };
5563
+ /** Nearest ancestor register that contributes a value or a deletion (clears abstain). */
5564
+ const nearestContributing = (path) => {
5565
+ for (let len = path.length - 1; len >= 0; len--) {
5566
+ const reg = registers.get(keyOf$1(path.slice(0, len)));
5567
+ if (reg?.result && reg.result.kind !== 'clear')
5568
+ return reg;
5569
+ }
5570
+ return undefined;
5571
+ };
5572
+ // graft with the deterministic type-change rule: a graft whose parent location is not a plain
5573
+ // record is DROPPED (the register stays intact and resurfaces if the container is restored)
5574
+ const graft = (tree, rel, res) => {
5575
+ if (!isContainer(tree))
5576
+ return tree;
5577
+ const head = String(rel[0]);
5578
+ if (rel.length === 1) {
5579
+ if (res.kind === 'delete') {
5580
+ if (!Object.hasOwn(tree, head))
5581
+ return tree;
5582
+ const copy = { ...tree };
5583
+ delete copy[head];
5584
+ return copy;
5585
+ }
5586
+ return { ...tree, [head]: res.value };
5587
+ }
5588
+ if (!Object.hasOwn(tree, head)) {
5589
+ // vivify an absent middle container so a checkpoint-seeded materialization matches a peer that
5590
+ // applied the ops incrementally (incremental apply creates missing parents). A numeric next
5591
+ // segment vivifies an array, else an object, mirroring the incremental apply path.
5592
+ const vivified = typeof rel[1] === 'number' ? [] : {};
5593
+ return { ...tree, [head]: graft(vivified, rel.slice(1), res) };
5594
+ }
5595
+ const child = graft(tree[head], rel.slice(1), res);
5596
+ return child === tree[head] ? tree : { ...tree, [head]: child };
5597
+ };
5598
+ /** Would a value at `rel` under `value` materialize, per the graft rules? */
5599
+ const graftable = (value, rel) => {
5600
+ let cur = value;
5601
+ for (let i = 0; i < rel.length - 1; i++) {
5602
+ if (!isContainer(cur) || !Object.hasOwn(cur, String(rel[i])))
5603
+ return false;
5604
+ cur = cur[String(rel[i])];
5605
+ }
5606
+ return isContainer(cur);
5607
+ };
5608
+ /**
5609
+ * Whether a value at `path` materializes: every contributing ancestor register down the
5610
+ * chain must be a `set` whose value composes containers to the next one. The drop rule is
5611
+ * checked against the WHOLE chain, since a graft fine under its nearest ancestor can still drop
5612
+ * at a scalar further up.
5613
+ */
5614
+ const shows = (path) => {
5615
+ let holder;
5616
+ for (let len = 0; len < path.length; len++) {
5617
+ const reg = registers.get(keyOf$1(path.slice(0, len)));
5618
+ if (!reg?.result || reg.result.kind === 'clear')
5619
+ continue;
5620
+ if (holder) {
5621
+ const hres = holder.result;
5622
+ if (!hres || hres.kind !== 'set')
5623
+ return false;
5624
+ if (!graftable(hres.value, reg.path.slice(holder.path.length))) {
5625
+ return false;
5626
+ }
5627
+ }
5628
+ holder = reg;
5629
+ }
5630
+ if (!holder)
5631
+ return true; // nothing above constrains → vivify semantics
5632
+ const hres = holder.result;
5633
+ if (!hres || hres.kind !== 'set')
5634
+ return false;
5635
+ return graftable(hres.value, path.slice(holder.path.length));
5636
+ };
5637
+ /** Deepest-live-wins subtree value: the register's fold value with every live descendant fold grafted on. */
5638
+ const materializeAt = (base) => {
5639
+ const res = base.result;
5640
+ let tree = res && res.kind === 'set' ? res.value : undefined;
5641
+ for (const d of descendantsOf(keyOf$1(base.path))) {
5642
+ const r = d.result;
5643
+ if (!r || r.kind === 'clear')
5644
+ continue;
5645
+ if (!shows(d.path))
5646
+ continue; // dropped under a deleted/scalar ancestor (matches applied deltas)
5647
+ tree = graft(tree, d.path.slice(base.path.length), r);
5648
+ }
5649
+ return tree;
5650
+ };
5651
+ const deltas = (changed) => {
5652
+ changed.sort((a, b) => a.reg.path.length - b.reg.path.length ||
5653
+ (keyOf$1(a.reg.path) < keyOf$1(b.reg.path) ? -1 : 1));
5654
+ const out = [];
5655
+ const regions = [];
5656
+ const covered = (key) => regions.some((r) => key === r || (r === '' ? true : key.startsWith(r + SEP)));
5657
+ for (const { reg, before } of changed) {
5658
+ const key = keyOf$1(reg.path);
5659
+ if (covered(key))
5660
+ continue;
5661
+ const res = reg.result;
5662
+ if (!res || res.kind === 'clear') {
5663
+ // the register now abstains: re-materialize the nearest ancestor region it cleared out of
5664
+ if (!reg.path.length)
5665
+ continue;
5666
+ const anc = nearestContributing(reg.path);
5667
+ const ares = anc?.result;
5668
+ if (!anc || !ares || ares.kind !== 'set' || !shows(anc.path))
5669
+ continue;
5670
+ out.push({ kind: 'set', path: anc.path, next: materializeAt(anc) });
5671
+ regions.push(keyOf$1(anc.path));
5672
+ continue;
5673
+ }
5674
+ if (!shows(reg.path))
5675
+ continue; // dropped by the type-change rule or a deleted parent
5676
+ if (res.kind === 'delete') {
5677
+ if (!reg.path.length)
5678
+ continue; // a root delete is meaningless
5679
+ out.push({
5680
+ kind: 'delete',
5681
+ path: reg.path,
5682
+ prev: before?.kind === 'set' ? before.value : undefined,
5683
+ });
5684
+ }
5685
+ else {
5686
+ out.push({ kind: 'set', path: reg.path, next: materializeAt(reg) });
5687
+ }
5688
+ regions.push(key);
5689
+ }
5690
+ return out;
5123
5691
  };
5124
5692
  return {
5125
5693
  ingest: (env, o) => {
5126
- const stamp = { hlc: env.hlc, writer: env.writer, origin: env.origin };
5127
- const out = [];
5694
+ const touched = new Map();
5695
+ const seq = ++ingestSeq;
5128
5696
  for (const op of env.ops) {
5697
+ if (o?.frontier && compareHlc(env.hlc, o.frontier) <= 0)
5698
+ continue; // below the pruned horizon
5699
+ // a delete or clear at the root has no parent register to abstain to; it can only blank the
5700
+ // whole document, and materialize would then disagree with the delta path, so drop it
5701
+ if (!op.path.length && op.kind !== 'set')
5702
+ continue;
5703
+ const reg = regAt(op.path);
5129
5704
  const key = keyOf$1(op.path);
5130
- let dominated = false;
5131
- let exact;
5132
- for (let len = 0; len <= op.path.length; len++) {
5133
- const reg = registers.get(keyOf$1(op.path.slice(0, len)));
5134
- if (!reg)
5705
+ if (!touched.has(key))
5706
+ touched.set(key, { reg, before: reg.result });
5707
+ const sop = op;
5708
+ for (const c of sop.cites ?? []) {
5709
+ // a self-citation (the op citing its own dot) would born-dead the write; ignore it
5710
+ if (c.origin === env.origin && compareHlc(c.hlc, env.hlc) === 0)
5135
5711
  continue;
5136
- if (len === op.path.length)
5137
- exact = reg;
5138
- else if (beats(reg, stamp)) {
5139
- dominated = true;
5140
- break;
5712
+ const cur = reg.water.get(c.origin);
5713
+ if (!cur || compareHlc(c.hlc, cur) > 0)
5714
+ reg.water.set(c.origin, c.hlc);
5715
+ }
5716
+ const best = reg.siblings.get(env.origin);
5717
+ if (!best || compareHlc(env.hlc, best.hlc) > 0) {
5718
+ const sib = {
5719
+ kind: op.kind,
5720
+ writer: env.writer,
5721
+ origin: env.origin,
5722
+ hlc: env.hlc,
5723
+ epoch: sop.epoch ?? 0,
5724
+ };
5725
+ if (op.kind === 'set')
5726
+ sib.value = op.next;
5727
+ if (op.kind !== 'clear' && Object.hasOwn(op, 'prev')) {
5728
+ sib.prev = op.prev;
5141
5729
  }
5730
+ reg.siblings.set(env.origin, sib);
5731
+ setSeq(key, env.origin, seq);
5142
5732
  }
5143
- if (dominated)
5733
+ if (o?.local && (sop.epoch ?? 0) > 0) {
5734
+ floors.set(key, Math.max(floors.get(key) ?? 0, sop.epoch));
5735
+ }
5736
+ }
5737
+ const changed = [];
5738
+ for (const c of touched.values()) {
5739
+ if (refresh(c.reg))
5740
+ changed.push(c);
5741
+ }
5742
+ if ((o?.local && !o?.reconcile) || !changed.length)
5743
+ return [];
5744
+ return deltas(changed);
5745
+ },
5746
+ stamp: (ops, o) => {
5747
+ const out = [];
5748
+ const bump = o?.bump ? 1 : 0;
5749
+ const frontier = o?.frontier;
5750
+ const epochFor = (key, live) => {
5751
+ let e = floors.get(key) ?? 0;
5752
+ for (const s of live)
5753
+ if (s.epoch > e)
5754
+ e = s.epoch;
5755
+ return e + bump;
5756
+ };
5757
+ for (const op of ops) {
5758
+ const key = keyOf$1(op.path);
5759
+ const reg = registers.get(key);
5760
+ const live = reg ? liveObserved(reg, frontier) : [];
5761
+ out.push({
5762
+ ...op,
5763
+ cites: live.map((s) => ({ origin: s.origin, hlc: s.hlc })),
5764
+ epoch: epochFor(key, live),
5765
+ });
5766
+ if (op.kind === 'clear')
5144
5767
  continue;
5145
- if (exact && beats(exact, stamp)) {
5146
- if (concurrentWith(op, exact.op)) {
5147
- const resolved = resolveConcurrent(exact.op, op, op.path);
5148
- if (resolved !== exact.op) {
5149
- exact.op = resolved;
5150
- if (!o?.local)
5151
- out.push(resolved);
5152
- }
5153
- }
5768
+ for (const d of descendantsOf(key)) {
5769
+ if (d.result?.kind === 'clear')
5770
+ continue; // already abstaining
5771
+ const dlive = liveObserved(d, frontier);
5772
+ if (!dlive.length)
5773
+ continue;
5774
+ out.push({
5775
+ kind: 'clear',
5776
+ path: d.path,
5777
+ cites: dlive.map((s) => ({ origin: s.origin, hlc: s.hlc })),
5778
+ epoch: epochFor(keyOf$1(d.path), dlive),
5779
+ });
5780
+ }
5781
+ }
5782
+ return out;
5783
+ },
5784
+ captureFrontier: () => ({ seq: ingestSeq }),
5785
+ liveAt: (path) => {
5786
+ const reg = registers.get(keyOf$1(path));
5787
+ return reg ? liveOf(reg) : [];
5788
+ },
5789
+ materialize: () => {
5790
+ const root = registers.get('');
5791
+ const res = root?.result;
5792
+ let tree = res && res.kind === 'set' ? res.value : undefined;
5793
+ for (const d of descendantsOf('')) {
5794
+ const r = d.result;
5795
+ if (!r || r.kind === 'clear')
5154
5796
  continue;
5797
+ if (!shows(d.path))
5798
+ continue; // dropped under a deleted/scalar ancestor (matches applied deltas)
5799
+ if (tree === undefined)
5800
+ tree = {}; // vivify: deeper registers materialize without a root write
5801
+ tree = graft(tree, d.path, r);
5802
+ }
5803
+ return tree;
5804
+ },
5805
+ checkpoint: () => {
5806
+ const out = [];
5807
+ for (const reg of registers.values()) {
5808
+ out.push({
5809
+ path: reg.path,
5810
+ siblings: [...reg.siblings.values()],
5811
+ water: Object.fromEntries(reg.water),
5812
+ });
5813
+ }
5814
+ return out;
5815
+ },
5816
+ load: (regs) => {
5817
+ const seq = ++ingestSeq;
5818
+ for (const r of regs) {
5819
+ const reg = regAt(r.path);
5820
+ const key = keyOf$1(r.path);
5821
+ for (const s of r.siblings) {
5822
+ const cur = reg.siblings.get(s.origin);
5823
+ if (!cur || compareHlc(s.hlc, cur.hlc) > 0) {
5824
+ reg.siblings.set(s.origin, s);
5825
+ setSeq(key, s.origin, seq);
5826
+ }
5155
5827
  }
5156
- let accepted = op;
5157
- if (exact && concurrentWith(op, exact.op)) {
5158
- accepted = resolveConcurrent(op, exact.op, op.path);
5828
+ for (const [o, h] of Object.entries(r.water)) {
5829
+ const cur = reg.water.get(o);
5830
+ if (!cur || compareHlc(h, cur) > 0)
5831
+ reg.water.set(o, h);
5159
5832
  }
5160
- const isDescendant = key === ''
5161
- ? (k) => k !== ''
5162
- : (k) => k.startsWith(key + SEP);
5163
- const replays = [];
5164
- for (const [k, reg] of registers) {
5165
- if (!isDescendant(k))
5166
- continue;
5167
- if (beats(stamp, reg))
5168
- registers.delete(k);
5169
- else
5170
- replays.push(reg);
5833
+ if (opt?.origin) {
5834
+ const own = reg.siblings.get(opt.origin);
5835
+ if (own) {
5836
+ floors.set(key, Math.max(floors.get(key) ?? 0, own.epoch));
5837
+ }
5838
+ }
5839
+ refresh(reg);
5840
+ }
5841
+ },
5842
+ prune: (frontier) => {
5843
+ for (const [key, reg] of [...registers]) {
5844
+ const sm = seqs.get(key);
5845
+ for (const [o, s] of [...reg.siblings]) {
5846
+ const w = reg.water.get(o);
5847
+ if (compareHlc(s.hlc, frontier) <= 0 &&
5848
+ w &&
5849
+ compareHlc(s.hlc, w) <= 0) {
5850
+ reg.siblings.delete(o);
5851
+ sm?.delete(o);
5852
+ }
5853
+ }
5854
+ for (const [o, h] of [...reg.water]) {
5855
+ if (compareHlc(h, frontier) <= 0)
5856
+ reg.water.delete(o);
5171
5857
  }
5172
- replays.sort(compareStamp);
5173
- registers.set(key, { hlc: env.hlc, writer: env.writer, origin: env.origin, op: accepted });
5174
- if (!o?.local) {
5175
- out.push(accepted);
5176
- for (const r of replays)
5177
- out.push(r.op);
5858
+ if (reg.siblings.size === 0 && reg.water.size === 0) {
5859
+ registers.delete(key);
5860
+ seqs.delete(key);
5861
+ floors.delete(key);
5178
5862
  }
5179
5863
  }
5180
- return out;
5864
+ const byDepth = [...registers.entries()].sort((a, b) => b[1].path.length - a[1].path.length);
5865
+ for (const [key, reg] of byDepth) {
5866
+ const live = liveOf(reg);
5867
+ if (live.length === 1 &&
5868
+ live[0].kind === 'delete' &&
5869
+ reg.siblings.size === 1 &&
5870
+ compareHlc(live[0].hlc, frontier) <= 0 &&
5871
+ tombstoneDroppable(key, reg)) {
5872
+ registers.delete(key);
5873
+ seqs.delete(key);
5874
+ floors.delete(key);
5875
+ }
5876
+ }
5877
+ },
5878
+ reset: () => {
5879
+ registers.clear();
5880
+ seqs.clear();
5181
5881
  },
5182
- reset: () => registers.clear(),
5183
5882
  };
5184
5883
  }
5185
5884
  function getAtPath(root, path) {
@@ -5192,7 +5891,7 @@ function getAtPath(root, path) {
5192
5891
  return cur;
5193
5892
  }
5194
5893
  /**
5195
- * The shared rebase routine (op-protocol RFC §5): invert pending, apply remote, re-apply
5894
+ * The shared rebase routine: invert pending, apply remote, re-apply
5196
5895
  * pending through the merge policies. Pure — branching's `rebase()` and the sequenced relay
5197
5896
  * client both call this.
5198
5897
  */
@@ -5207,6 +5906,10 @@ function rebaseOps(root, pending, remote, policies) {
5207
5906
  for (const batch of pending) {
5208
5907
  const next = [];
5209
5908
  for (const op of batch) {
5909
+ if (op.kind === 'clear') {
5910
+ next.push(op); // a register intent, not a value change: passes through untouched
5911
+ continue;
5912
+ }
5210
5913
  const cur = getAtPath(base, op.path);
5211
5914
  if (op.kind === 'delete') {
5212
5915
  next.push({ kind: 'delete', path: op.path, prev: cur });
@@ -5245,40 +5948,108 @@ function generateOrigin() {
5245
5948
  }
5246
5949
  /**
5247
5950
  * Wires a copy-on-write signal (a `store` root) to the op protocol: local writes emit
5248
- * stamped envelopes, received envelopes fold in through the converging apply. The
5249
- * unsequenced-topology client core that `tabSync(store)` and P2P transports build on.
5951
+ * stamped envelopes (citing the sibling dots they observed), received envelopes fold in
5952
+ * through the converging register. The unsequenced-topology client core that
5953
+ * `tabSync(store)` and P2P transports build on.
5250
5954
  */
5251
5955
  const RECENT_LOCAL_CAP = 64;
5252
5956
  function opSync(source, opt) {
5253
5957
  const origin = opt.origin ?? generateOrigin();
5254
5958
  const clock = opt.clock ?? createHlcClock();
5255
- const conv = createConvergingApply({ policies: opt.policies });
5959
+ const conv = createConvergingApply({
5960
+ policies: opt.policies,
5961
+ folds: opt.folds,
5962
+ origin,
5963
+ });
5256
5964
  const subscribers = new Set();
5965
+ // per-origin high-watermark; `versions.get(origin)` IS the local emit counter, so a hydrate/restore
5966
+ // that raises our own watermark also advances the next mint — no separate counter to drift out of
5967
+ // sync and collide with a version acked before a reboot but dropped from a debounced outbox.
5257
5968
  const versions = new Map();
5258
5969
  const recentLocal = [];
5259
- let version = 0;
5970
+ // highest stability frontier this peer has pruned to. A remote envelope at or below it is a settled
5971
+ // straggler (its state is compacted away); re-admitting one could resurrect a value below the
5972
+ // frontier, and per-origin version dedup cannot catch a FIRST-CONTACT straggler (no prior entry),
5973
+ // so the frontier is the admission gate that closes that hole on the receive path.
5974
+ let prunedFrontier;
5975
+ const resolvedInjector = opt.driver
5976
+ ? null
5977
+ : (opt.injector ?? inject(Injector));
5260
5978
  const log = opLog(source, opt.driver
5261
5979
  ? { origin, driver: opt.driver }
5262
- : { origin, injector: opt.injector ?? inject(Injector) });
5980
+ : { origin, injector: resolvedInjector });
5981
+ // Local envelopes stamped + registered but not yet handed to the transport. A `receive` freezes
5982
+ // this peer's pending writes here so it can ingest the remote WITHOUT emitting mid-receive — the
5983
+ // synchronous re-entrant emission that used to scramble the relay's commit order. The outbox
5984
+ // drains on a LATER tick, so emission always lands outside any receive callstack. Writes made
5985
+ // while a drain is still owed queue here too, keeping wire order == version order (else a receiver
5986
+ // would dedup the older, still-frozen envelope).
5987
+ //
5988
+ // Deferral rides an Angular effect, so it arms only on the injector path — the transport
5989
+ // topologies (`tabSync`, `meshSync`) that actually re-enter through a relay. A custom `driver`
5990
+ // (worker mirror, pure sim, multi-reader) owns its own scheduling and has no such re-entrancy, so
5991
+ // it emits synchronously; the freeze-before-observe STAMPING fix below is identical either way.
5992
+ const canDefer = !opt.driver;
5993
+ const outbox = [];
5994
+ let receiving = false;
5995
+ let bumping = false;
5996
+ // set while a synced fork's commit is emitting: freezes emission cites to what the fork observed
5997
+ let scopeFrontier;
5998
+ // a signal the drain reaction tracks; bumping it schedules an outbox drain for the next tick
5999
+ const drainTick = signal(0, ...(ngDevMode ? [{ debugName: "drainTick" }] : []));
6000
+ const scheduleDrain = () => drainTick.update((v) => v + 1);
6001
+ const notify = (env) => {
6002
+ for (const cb of [...subscribers])
6003
+ cb(env);
6004
+ };
6005
+ const drainOutbox = () => {
6006
+ for (const env of outbox.splice(0))
6007
+ notify(env);
6008
+ };
5263
6009
  const emitLocal = (ops) => {
6010
+ const frontier = scopeFrontier;
6011
+ const stamped = conv.stamp(ops, { bump: bumping, frontier });
6012
+ const nextVersion = (versions.get(origin) ?? 0) + 1;
5264
6013
  const env = {
5265
6014
  proto: OP_PROTO_VERSION,
5266
6015
  origin,
5267
6016
  writer: opt.writer,
5268
- version: ++version,
6017
+ version: nextVersion,
5269
6018
  hlc: clock.next(),
5270
6019
  policyVersion: opt.policyVersion ?? 0,
5271
- ops,
6020
+ ops: stamped,
5272
6021
  };
5273
- versions.set(origin, env.version);
5274
- conv.ingest(env, { local: true });
6022
+ versions.set(origin, nextVersion);
6023
+ if (frontier) {
6024
+ // a fork commit: its ops are concurrent siblings (they cite only the fork-time frontier), so
6025
+ // move the store to the fold winner rather than leaving the raw committed value in place
6026
+ const reconciled = conv.ingest(env, { local: true, reconcile: true });
6027
+ if (reconciled.length)
6028
+ log.apply(reconciled);
6029
+ }
6030
+ else {
6031
+ conv.ingest(env, { local: true });
6032
+ }
5275
6033
  recentLocal.push(env);
5276
6034
  if (recentLocal.length > RECENT_LOCAL_CAP)
5277
6035
  recentLocal.shift();
5278
- for (const cb of [...subscribers])
5279
- cb(env);
6036
+ // mid-receive, or a drain still owed → queue (frozen stamp verbatim) instead of emitting now
6037
+ if (canDefer && (receiving || outbox.length)) {
6038
+ outbox.push(env);
6039
+ scheduleDrain();
6040
+ return;
6041
+ }
6042
+ notify(env);
5280
6043
  };
5281
6044
  const unsub = log.subscribe((batch) => emitLocal(batch.ops));
6045
+ // fires on the tick after `scheduleDrain`: emits frozen local envelopes outside any receive frame
6046
+ const drainRun = () => {
6047
+ drainTick();
6048
+ untracked(drainOutbox);
6049
+ };
6050
+ const drainRef = canDefer
6051
+ ? effect(drainRun, { injector: resolvedInjector })
6052
+ : null;
5282
6053
  return {
5283
6054
  origin,
5284
6055
  subscribe: (cb) => {
@@ -5290,11 +6061,25 @@ function opSync(source, opt) {
5290
6061
  return;
5291
6062
  if (env.proto !== OP_PROTO_VERSION) {
5292
6063
  if (isDevMode()) {
5293
- console.warn(`[@mmstack/primitives] dropped envelope with proto ${env.proto} (expected ${OP_PROTO_VERSION})`);
6064
+ 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)`);
5294
6065
  }
5295
6066
  return;
5296
6067
  }
5297
- clock.observe(env.hlc);
6068
+ const reason = validateEnvelope(env);
6069
+ if (reason !== null) {
6070
+ if (isDevMode()) {
6071
+ console.warn(`[@mmstack/primitives] dropped malformed envelope (${reason}) from origin ${String(env.origin)}`);
6072
+ }
6073
+ opt.onReject?.(env, reason);
6074
+ return;
6075
+ }
6076
+ // a settled straggler at or below the pruned stability frontier: reject it (its state is
6077
+ // compacted, re-admitting could resurrect a below-frontier value). All ops in an envelope share
6078
+ // its stamp, so the envelope hlc is the dot for every op. The live relay path never delivers a
6079
+ // below-frontier op (a lagging client gets a snapshot, not a delta), so this only fires on a
6080
+ // stray re-broadcast, e.g. over a P2P/multi-path topology.
6081
+ if (prunedFrontier && compareHlc(env.hlc, prunedFrontier) <= 0)
6082
+ return;
5298
6083
  const known = versions.get(env.origin);
5299
6084
  if (known !== undefined && env.version <= known)
5300
6085
  return; // duplicate/covered — idempotent
@@ -5302,43 +6087,140 @@ function opSync(source, opt) {
5302
6087
  opt.onGap?.(env.origin, known + 1, env.version);
5303
6088
  }
5304
6089
  versions.set(env.origin, env.version);
6090
+ receiving = true;
6091
+ try {
6092
+ log.flush();
6093
+ clock.observe(env.hlc);
6094
+ const ops = conv.ingest(env);
6095
+ if (ops.length)
6096
+ log.apply(ops);
6097
+ }
6098
+ finally {
6099
+ receiving = false;
6100
+ }
6101
+ },
6102
+ flush: () => {
6103
+ drainOutbox();
5305
6104
  log.flush();
5306
- const ops = conv.ingest(env);
5307
- if (ops.length)
5308
- log.apply(ops);
5309
6105
  },
5310
- flush: () => log.flush(),
6106
+ override: (fn) => {
6107
+ log.flush(); // earlier pending writes emit un-bumped
6108
+ bumping = true;
6109
+ try {
6110
+ fn();
6111
+ log.flush();
6112
+ }
6113
+ finally {
6114
+ bumping = false;
6115
+ }
6116
+ },
6117
+ captureFrontier: () => {
6118
+ log.flush(); // fold pending base writes in first, so they count as observed
6119
+ return conv.captureFrontier();
6120
+ },
6121
+ commitScope: (frontier, fn) => {
6122
+ log.flush(); // earlier pending writes emit against the live frontier, not this one
6123
+ scopeFrontier = frontier;
6124
+ try {
6125
+ fn();
6126
+ log.flush(); // stamp + register the scoped writes now, while the frontier is frozen
6127
+ }
6128
+ finally {
6129
+ scopeFrontier = undefined;
6130
+ }
6131
+ },
5311
6132
  watermark: () => Object.fromEntries(versions),
6133
+ prune: (frontier) => {
6134
+ if (!prunedFrontier || compareHlc(frontier, prunedFrontier) > 0) {
6135
+ prunedFrontier = frontier;
6136
+ }
6137
+ conv.prune(frontier);
6138
+ },
5312
6139
  snapshot: () => {
5313
6140
  log.flush();
5314
- return { root: untracked(source), wm: Object.fromEntries(versions) };
6141
+ return {
6142
+ root: untracked(source),
6143
+ registers: conv.checkpoint(),
6144
+ wm: Object.fromEntries(versions),
6145
+ };
5315
6146
  },
5316
6147
  seed: () => {
5317
6148
  log.flush();
5318
6149
  emitLocal([{ kind: 'set', path: [], next: untracked(source) }]);
5319
6150
  },
5320
- hydrate: (root, wm) => {
6151
+ hydrate: (state, pending) => {
5321
6152
  log.flush();
5322
- const covered = wm?.[origin] ?? 0;
5323
- const pending = recentLocal.filter((e) => e.version > covered);
6153
+ // rebase this origin's uncovered local writes on top. A caller that keeps a durable outbox
6154
+ // (meshSync, the worker replica) passes its full unacked set, so a long offline burst larger
6155
+ // than the in-memory `recentLocal` cap is never dropped from the rebase; without it, fall back
6156
+ // to the recent-local ring.
6157
+ const source = pending ?? recentLocal;
6158
+ const toReplay = source.filter((e) => e.version > (state.wm?.[e.origin] ?? 0));
5324
6159
  conv.reset();
5325
- let next = root;
5326
- for (const e of pending)
5327
- next = applyOps(next, e.ops);
5328
- log.apply([{ kind: 'set', path: [], next }]);
5329
- for (const [o, v] of Object.entries(wm ?? {})) {
6160
+ conv.load(state.registers ?? []);
6161
+ const deltas = [];
6162
+ for (const e of toReplay) {
6163
+ deltas.push(...conv.ingest(e, { local: true, reconcile: true }));
6164
+ }
6165
+ log.apply([
6166
+ { kind: 'set', path: [], next: applyOps(state.root, deltas) },
6167
+ ]);
6168
+ for (const [o, v] of Object.entries(state.wm ?? {})) {
5330
6169
  versions.set(o, Math.max(versions.get(o) ?? 0, v));
5331
6170
  }
5332
- for (const e of pending)
5333
- conv.ingest(e, { local: true });
6171
+ },
6172
+ restore: (envs, highWater) => {
6173
+ let tailOrigin;
6174
+ for (const env of envs) {
6175
+ clock.observe(env.hlc); // keep the clock ≥ restored stamps before any future mint
6176
+ log.apply(env.ops); // reflect the offline edit in the store, echo-free
6177
+ conv.ingest(env, { local: true }); // register as a local winner (survives a reconnect merge)
6178
+ recentLocal.push(env);
6179
+ if (recentLocal.length > RECENT_LOCAL_CAP)
6180
+ recentLocal.shift();
6181
+ versions.set(env.origin, Math.max(versions.get(env.origin) ?? 0, env.version));
6182
+ tailOrigin = env.origin;
6183
+ notify(env); // hand to the transport to resend the unacknowledged tail
6184
+ }
6185
+ if (highWater != null && tailOrigin != null) {
6186
+ versions.set(tailOrigin, Math.max(versions.get(tailOrigin) ?? 0, highWater));
6187
+ }
5334
6188
  },
5335
6189
  destroy: () => {
6190
+ drainOutbox(); // don't silently drop frozen-but-unsent local writes
5336
6191
  unsub();
5337
6192
  subscribers.clear();
6193
+ drainRef?.destroy();
5338
6194
  log.destroy();
5339
6195
  },
5340
6196
  };
5341
6197
  }
6198
+ /**
6199
+ * Fork a synced store for isolated edits (an agent branch, a staged review), keeping the correct
6200
+ * emission semantics on commit. The fork observes the base as it was when this call ran; committing
6201
+ * emits its diff citing only those observed dots, so an edit that landed on the base mid-flight
6202
+ * stays a concurrent sibling and the configured fold decides between them, rather than the commit
6203
+ * overwriting a write it never saw. `rebase()` re-observes the base (a following commit then
6204
+ * supersedes what is visible now, the reviewed-and-apply step). Pass the same `store` and `sync`
6205
+ * that are wired together; the fork is a plain {@link Fork} otherwise, so `forkStore` itself stays
6206
+ * sync-agnostic.
6207
+ */
6208
+ function syncedFork(sync, store, opt) {
6209
+ let frontier = sync.captureFrontier();
6210
+ const f = forkStore(store, opt);
6211
+ return {
6212
+ store: f.store,
6213
+ ops: f.ops,
6214
+ commit: () => sync.commitScope(frontier, () => f.commit()),
6215
+ discard: () => {
6216
+ f.discard();
6217
+ frontier = sync.captureFrontier();
6218
+ },
6219
+ rebase: () => {
6220
+ frontier = sync.captureFrontier();
6221
+ },
6222
+ };
6223
+ }
5342
6224
 
5343
6225
  /**
5344
6226
  * Undo/redo for a copy-on-write store, built on the op-log: each tracked change is stored as
@@ -5609,7 +6491,7 @@ const isPlainArray = (v) => Array.isArray(v) && !isOpaque(v);
5609
6491
  function keyOf(item, key) {
5610
6492
  if (typeof key === 'function')
5611
6493
  return key(item);
5612
- return isRecord(item) ? item[key] : item;
6494
+ return isRecord$1(item) ? item[key] : item;
5613
6495
  }
5614
6496
  /**
5615
6497
  * Produces a value equal to `next` but sharing as much of `prev`'s reference structure as possible:
@@ -5638,7 +6520,7 @@ function reconcileValue(prev, next, key) {
5638
6520
  });
5639
6521
  return changed ? out : prev;
5640
6522
  }
5641
- if (isRecord(prev) && isRecord(next)) {
6523
+ if (isRecord$1(prev) && isRecord$1(next)) {
5642
6524
  const nextKeys = Object.keys(next);
5643
6525
  let changed = Object.keys(prev).length !== nextKeys.length;
5644
6526
  const out = {};
@@ -5898,7 +6780,7 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
5898
6780
  return writable;
5899
6781
  }
5900
6782
 
5901
- /** Op-mode sync for a writable store: hello exchange, then live envelopes (RFC §6 tab flavor). */
6783
+ /** Op-mode sync for a writable store: hello exchange, then live envelopes. */
5902
6784
  function storeTabSync(sig, opt, bus, injector) {
5903
6785
  const sync = opSync(sig, {
5904
6786
  writer: opt.writer ?? 'local',
@@ -5942,7 +6824,7 @@ function storeTabSync(sig, opt, bus, injector) {
5942
6824
  const covered = Object.entries(snap.wm).every(([origin, v]) => (msg.wm[origin] ?? 0) >= v);
5943
6825
  post(covered
5944
6826
  ? { t: 'uptodate', to: msg.from }
5945
- : { t: 'state', to: msg.from, root: snap.root, wm: snap.wm });
6827
+ : { t: 'state', to: msg.from, state: snap });
5946
6828
  }, Math.random() * jitterMs);
5947
6829
  responseTimers.set(msg.from, timer);
5948
6830
  return;
@@ -5957,7 +6839,7 @@ function storeTabSync(sig, opt, bus, injector) {
5957
6839
  if (msg.to !== sync.origin || phase !== 'joining')
5958
6840
  return;
5959
6841
  if (msg.t === 'state')
5960
- sync.hydrate(msg.root, msg.wm);
6842
+ sync.hydrate(msg.state);
5961
6843
  goLive();
5962
6844
  return;
5963
6845
  }
@@ -5980,6 +6862,12 @@ function storeTabSync(sig, opt, bus, injector) {
5980
6862
  class MessageBus {
5981
6863
  channel = new BroadcastChannel('mmstack-tab-sync-bus');
5982
6864
  listeners = new Map();
6865
+ constructor() {
6866
+ inject(DestroyRef).onDestroy(() => {
6867
+ this.channel.close();
6868
+ this.listeners.clear();
6869
+ });
6870
+ }
5983
6871
  subscribe(id, listener) {
5984
6872
  const wrapped = (ev) => {
5985
6873
  try {
@@ -6010,10 +6898,6 @@ class MessageBus {
6010
6898
  post: (value) => this.channel.postMessage({ id, value }),
6011
6899
  };
6012
6900
  }
6013
- ngOnDestroy() {
6014
- this.channel.close();
6015
- this.listeners.clear();
6016
- }
6017
6901
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
6018
6902
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: MessageBus, providedIn: 'root' });
6019
6903
  }
@@ -6022,7 +6906,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
6022
6906
  args: [{
6023
6907
  providedIn: 'root',
6024
6908
  }]
6025
- }] });
6909
+ }], ctorParameters: () => [] });
6026
6910
  /**
6027
6911
  * @deprecated The generated id hashes the call-site stack line, which collides when a shared
6028
6912
  * helper calls {@link tabSync} for multiple signals and diverges across minified builds during
@@ -6087,7 +6971,7 @@ function tabSync(sig, opt) {
6087
6971
  if (isPlatformServer(injector.get(PLATFORM_ID)))
6088
6972
  return sig;
6089
6973
  const id = typeof opt === 'string' ? opt : (opt?.id ?? generateDeterministicID());
6090
- const bus = injector.get(MessageBus);
6974
+ const bus = optObj?.bus ?? injector.get(MessageBus);
6091
6975
  const storeKind = sig[STORE_KIND];
6092
6976
  if (storeKind === 'writable') {
6093
6977
  storeTabSync(sig, { ...optObj, id }, bus, injector);
@@ -6329,5 +7213,5 @@ function withHistory(sourceOrValue, opt) {
6329
7213
  * Generated bundle index. Do not edit.
6330
7214
  */
6331
7215
 
6332
- 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 };
7216
+ 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 };
6333
7217
  //# sourceMappingURL=mmstack-primitives.mjs.map