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