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