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