@mmstack/primitives 20.14.4 → 20.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/mmstack-primitives.mjs +43 -62
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +18 -5
- package/package.json +1 -1
|
@@ -3582,7 +3582,7 @@ function createPointerDrag(opt) {
|
|
|
3582
3582
|
return base;
|
|
3583
3583
|
}
|
|
3584
3584
|
const hostRef = inject((ElementRef), { optional: true });
|
|
3585
|
-
const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], stopPropagation = false, debugName = 'pointerDrag', } = opt ?? {};
|
|
3585
|
+
const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], stopPropagation = false, capture = false, debugName = 'pointerDrag', } = opt ?? {};
|
|
3586
3586
|
const resolve = (t) => {
|
|
3587
3587
|
if (!t)
|
|
3588
3588
|
return null;
|
|
@@ -3714,6 +3714,7 @@ function createPointerDrag(opt) {
|
|
|
3714
3714
|
const controller = new AbortController();
|
|
3715
3715
|
el.addEventListener('pointerdown', onDown(el), {
|
|
3716
3716
|
signal: controller.signal,
|
|
3717
|
+
capture,
|
|
3717
3718
|
});
|
|
3718
3719
|
return () => {
|
|
3719
3720
|
controller.abort();
|
|
@@ -4244,7 +4245,7 @@ function generateOrigin$1() {
|
|
|
4244
4245
|
return globalThis.crypto.randomUUID();
|
|
4245
4246
|
return Math.random().toString(36).substring(2);
|
|
4246
4247
|
}
|
|
4247
|
-
const isPlainArray$
|
|
4248
|
+
const isPlainArray$2 = (v) => Array.isArray(v) && !isOpaque(v);
|
|
4248
4249
|
/**
|
|
4249
4250
|
* Reference-identity-pruned structural diff — the same short-circuit discipline as `merge3`:
|
|
4250
4251
|
* an untouched subtree kept its reference (the store's copy-on-write contract), so the walk
|
|
@@ -4269,7 +4270,7 @@ function diffNode(prev, next, path, ops) {
|
|
|
4269
4270
|
}
|
|
4270
4271
|
return;
|
|
4271
4272
|
}
|
|
4272
|
-
if (isPlainArray$
|
|
4273
|
+
if (isPlainArray$2(prev) && isPlainArray$2(next)) {
|
|
4273
4274
|
// same length → per-index descent (matches `arr[i].x.set(...)` writes); a length
|
|
4274
4275
|
// change is a whole unit — index attribution lies under insert/remove/reorder
|
|
4275
4276
|
if (prev.length === next.length) {
|
|
@@ -4288,7 +4289,7 @@ function applyAt(container, path, idx, op) {
|
|
|
4288
4289
|
const seg = path[idx];
|
|
4289
4290
|
if (seg === '__proto__')
|
|
4290
4291
|
return container;
|
|
4291
|
-
const base = isPlainArray$
|
|
4292
|
+
const base = isPlainArray$2(container)
|
|
4292
4293
|
? container.slice()
|
|
4293
4294
|
: isRecord$1(container)
|
|
4294
4295
|
? { ...container }
|
|
@@ -5218,12 +5219,15 @@ function evenPositions(n) {
|
|
|
5218
5219
|
}
|
|
5219
5220
|
|
|
5220
5221
|
/**
|
|
5221
|
-
* Wire protocol version. Version 2 ops carry `cites` + `epoch` (the dot-citation register)
|
|
5222
|
-
*
|
|
5223
|
-
*
|
|
5224
|
-
*
|
|
5222
|
+
* Wire protocol version. Version 2 ops carry `cites` + `epoch` (the dot-citation register).
|
|
5223
|
+
* Version 3 changes MATERIALIZATION semantics, not shape: grafts descend through plain arrays
|
|
5224
|
+
* (per-index ops materialize instead of silently dropping) and the documented drop rule is
|
|
5225
|
+
* enforced for non-plain ancestors — so a v2 replica and a v3 replica CANNOT converge on the
|
|
5226
|
+
* same op set, which is exactly what the version fence exists to make loud. Envelopes from
|
|
5227
|
+
* other versions are dropped loudly, and the tab-sync join protocol refuses to pair mismatched
|
|
5228
|
+
* peers: versions are never silently mixed, in shape OR in meaning.
|
|
5225
5229
|
*/
|
|
5226
|
-
const OP_PROTO_VERSION =
|
|
5230
|
+
const OP_PROTO_VERSION = 3;
|
|
5227
5231
|
const CONFLICT_BRAND = '~mmstackConflict';
|
|
5228
5232
|
function isConflicted(value) {
|
|
5229
5233
|
return typeof value === 'object' && value !== null && CONFLICT_BRAND in value;
|
|
@@ -5480,7 +5484,14 @@ const mergeFold = (merge) => {
|
|
|
5480
5484
|
return { kind: 'set', value: acc };
|
|
5481
5485
|
};
|
|
5482
5486
|
};
|
|
5483
|
-
const
|
|
5487
|
+
const isPlainArray$1 = (v) => Array.isArray(v) && !isOpaque(v);
|
|
5488
|
+
/**
|
|
5489
|
+
* The graft-side container admission. MUST equal `diffNode`'s descent set (plain records and
|
|
5490
|
+
* plain non-opaque arrays) and `applyAt`'s copy set: emission, incremental apply and
|
|
5491
|
+
* checkpoint materialization decide "container or leaf" identically, or a checkpoint-seeded
|
|
5492
|
+
* replica and an incrementally-applied one disagree about the same op set.
|
|
5493
|
+
*/
|
|
5494
|
+
const isContainer = (v) => isPlainArray$1(v) || isRecord$1(v);
|
|
5484
5495
|
/**
|
|
5485
5496
|
* The unsequenced-topology convergence core: a dot-citation multi-value register per path.
|
|
5486
5497
|
* An op supersedes exactly the sibling dots it cites; uncited concurrent writes stay live; a
|
|
@@ -5539,8 +5550,6 @@ function createConvergingApply(opt) {
|
|
|
5539
5550
|
}
|
|
5540
5551
|
return out.sort((a, b) => a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0);
|
|
5541
5552
|
};
|
|
5542
|
-
// The live siblings a frontier had observed: those that arrived at or below its captured seq.
|
|
5543
|
-
// Used by a fork commit so it supersedes only what it saw when it forked, not later writes.
|
|
5544
5553
|
const liveObserved = (reg, frontier) => {
|
|
5545
5554
|
const live = liveOf(reg);
|
|
5546
5555
|
if (!frontier)
|
|
@@ -5548,11 +5557,6 @@ function createConvergingApply(opt) {
|
|
|
5548
5557
|
const sm = seqs.get(keyOf$1(reg.path));
|
|
5549
5558
|
return live.filter((s) => (sm?.get(s.origin) ?? 0) <= frontier.seq);
|
|
5550
5559
|
};
|
|
5551
|
-
// JSON of a tuple array, not a separator-joined string: `origin` is a caller-supplied value on a
|
|
5552
|
-
// P2P peer, so a naive `origin@p.l#epoch` join lets a crafted origin collide the signatures of two
|
|
5553
|
-
// distinct live sets. A collision makes refresh() skip a fold update, and since that skip is
|
|
5554
|
-
// arrival-order-sensitive it breaks convergence. JSON.stringify escapes the strings and the array
|
|
5555
|
-
// structure is unambiguous, so the signature is injective in the live set.
|
|
5556
5560
|
const sigOf = (live) => JSON.stringify(live.map((s) => [s.origin, s.hlc.p, s.hlc.l, s.epoch, s.kind]));
|
|
5557
5561
|
/** Recompute the fold cache; true iff the materialized result meaningfully changed. */
|
|
5558
5562
|
const refresh = (reg) => {
|
|
@@ -5602,9 +5606,6 @@ function createConvergingApply(opt) {
|
|
|
5602
5606
|
}
|
|
5603
5607
|
return true;
|
|
5604
5608
|
};
|
|
5605
|
-
// A lone tombstone is droppable only if nothing else still materializes its key: no live
|
|
5606
|
-
// descendant register would resurface, and no live ancestor `set` value still holds it. Mirrors
|
|
5607
|
-
// the relay's retention twin so a client that prunes converges with a joiner seeded from the relay.
|
|
5608
5609
|
const tombstoneDroppable = (key, reg) => {
|
|
5609
5610
|
for (const [k, other] of registers) {
|
|
5610
5611
|
if (k === key)
|
|
@@ -5632,31 +5633,37 @@ function createConvergingApply(opt) {
|
|
|
5632
5633
|
}
|
|
5633
5634
|
return undefined;
|
|
5634
5635
|
};
|
|
5635
|
-
// graft with the deterministic type-change rule: a graft whose parent location is not a plain
|
|
5636
|
-
// record is DROPPED (the register stays intact and resurfaces if the container is restored)
|
|
5637
5636
|
const graft = (tree, rel, res) => {
|
|
5638
5637
|
if (!isContainer(tree))
|
|
5639
5638
|
return tree;
|
|
5640
5639
|
const head = String(rel[0]);
|
|
5640
|
+
if (head === '__proto__')
|
|
5641
|
+
return tree;
|
|
5642
|
+
const copyOf = () => (isPlainArray$1(tree) ? tree.slice() : { ...tree });
|
|
5641
5643
|
if (rel.length === 1) {
|
|
5642
5644
|
if (res.kind === 'delete') {
|
|
5643
5645
|
if (!Object.hasOwn(tree, head))
|
|
5644
5646
|
return tree;
|
|
5645
|
-
const copy =
|
|
5647
|
+
const copy = copyOf();
|
|
5646
5648
|
delete copy[head];
|
|
5647
5649
|
return copy;
|
|
5648
5650
|
}
|
|
5649
|
-
|
|
5651
|
+
const copy = copyOf();
|
|
5652
|
+
copy[head] = res.value;
|
|
5653
|
+
return copy;
|
|
5650
5654
|
}
|
|
5651
5655
|
if (!Object.hasOwn(tree, head)) {
|
|
5652
|
-
// vivify an absent middle container so a checkpoint-seeded materialization matches a peer that
|
|
5653
|
-
// applied the ops incrementally (incremental apply creates missing parents). A numeric next
|
|
5654
|
-
// segment vivifies an array, else an object, mirroring the incremental apply path.
|
|
5655
5656
|
const vivified = typeof rel[1] === 'number' ? [] : {};
|
|
5656
|
-
|
|
5657
|
+
const copy = copyOf();
|
|
5658
|
+
copy[head] = graft(vivified, rel.slice(1), res);
|
|
5659
|
+
return copy;
|
|
5657
5660
|
}
|
|
5658
5661
|
const child = graft(tree[head], rel.slice(1), res);
|
|
5659
|
-
|
|
5662
|
+
if (child === tree[head])
|
|
5663
|
+
return tree;
|
|
5664
|
+
const copy = copyOf();
|
|
5665
|
+
copy[head] = child;
|
|
5666
|
+
return copy;
|
|
5660
5667
|
};
|
|
5661
5668
|
/** Would a value at `rel` under `value` materialize, per the graft rules? */
|
|
5662
5669
|
const graftable = (value, rel) => {
|
|
@@ -5758,9 +5765,7 @@ function createConvergingApply(opt) {
|
|
|
5758
5765
|
const seq = ++ingestSeq;
|
|
5759
5766
|
for (const op of env.ops) {
|
|
5760
5767
|
if (o?.frontier && compareHlc(env.hlc, o.frontier) <= 0)
|
|
5761
|
-
continue;
|
|
5762
|
-
// a delete or clear at the root has no parent register to abstain to; it can only blank the
|
|
5763
|
-
// whole document, and materialize would then disagree with the delta path, so drop it
|
|
5768
|
+
continue;
|
|
5764
5769
|
if (!op.path.length && op.kind !== 'set')
|
|
5765
5770
|
continue;
|
|
5766
5771
|
const reg = regAt(op.path);
|
|
@@ -6025,15 +6030,8 @@ function opSync(source, opt) {
|
|
|
6025
6030
|
origin,
|
|
6026
6031
|
});
|
|
6027
6032
|
const subscribers = new Set();
|
|
6028
|
-
// per-origin high-watermark; `versions.get(origin)` IS the local emit counter, so a hydrate/restore
|
|
6029
|
-
// that raises our own watermark also advances the next mint — no separate counter to drift out of
|
|
6030
|
-
// sync and collide with a version acked before a reboot but dropped from a debounced outbox.
|
|
6031
6033
|
const versions = new Map();
|
|
6032
6034
|
const recentLocal = [];
|
|
6033
|
-
// highest stability frontier this peer has pruned to. A remote envelope at or below it is a settled
|
|
6034
|
-
// straggler (its state is compacted away); re-admitting one could resurrect a value below the
|
|
6035
|
-
// frontier, and per-origin version dedup cannot catch a FIRST-CONTACT straggler (no prior entry),
|
|
6036
|
-
// so the frontier is the admission gate that closes that hole on the receive path.
|
|
6037
6035
|
let prunedFrontier;
|
|
6038
6036
|
const resolvedInjector = opt.driver
|
|
6039
6037
|
? null
|
|
@@ -6041,17 +6039,6 @@ function opSync(source, opt) {
|
|
|
6041
6039
|
const log = opLog(source, opt.driver
|
|
6042
6040
|
? { origin, driver: opt.driver }
|
|
6043
6041
|
: { origin, injector: resolvedInjector });
|
|
6044
|
-
// Local envelopes stamped + registered but not yet handed to the transport. A `receive` freezes
|
|
6045
|
-
// this peer's pending writes here so it can ingest the remote WITHOUT emitting mid-receive — the
|
|
6046
|
-
// synchronous re-entrant emission that used to scramble the relay's commit order. The outbox
|
|
6047
|
-
// drains on a LATER tick, so emission always lands outside any receive callstack. Writes made
|
|
6048
|
-
// while a drain is still owed queue here too, keeping wire order == version order (else a receiver
|
|
6049
|
-
// would dedup the older, still-frozen envelope).
|
|
6050
|
-
//
|
|
6051
|
-
// Deferral rides an Angular effect, so it arms only on the injector path — the transport
|
|
6052
|
-
// topologies (`tabSync`, `meshSync`) that actually re-enter through a relay. A custom `driver`
|
|
6053
|
-
// (worker mirror, pure sim, multi-reader) owns its own scheduling and has no such re-entrancy, so
|
|
6054
|
-
// it emits synchronously; the freeze-before-observe STAMPING fix below is identical either way.
|
|
6055
6042
|
const canDefer = !opt.driver;
|
|
6056
6043
|
const outbox = [];
|
|
6057
6044
|
let receiving = false;
|
|
@@ -6136,11 +6123,6 @@ function opSync(source, opt) {
|
|
|
6136
6123
|
opt.onReject?.(env, reason);
|
|
6137
6124
|
return;
|
|
6138
6125
|
}
|
|
6139
|
-
// a settled straggler at or below the pruned stability frontier: reject it (its state is
|
|
6140
|
-
// compacted, re-admitting could resurrect a below-frontier value). All ops in an envelope share
|
|
6141
|
-
// its stamp, so the envelope hlc is the dot for every op. The live relay path never delivers a
|
|
6142
|
-
// below-frontier op (a lagging client gets a snapshot, not a delta), so this only fires on a
|
|
6143
|
-
// stray re-broadcast, e.g. over a P2P/multi-path topology.
|
|
6144
6126
|
if (prunedFrontier && compareHlc(env.hlc, prunedFrontier) <= 0)
|
|
6145
6127
|
return;
|
|
6146
6128
|
const known = versions.get(env.origin);
|
|
@@ -6213,10 +6195,6 @@ function opSync(source, opt) {
|
|
|
6213
6195
|
},
|
|
6214
6196
|
hydrate: (state, pending) => {
|
|
6215
6197
|
log.flush();
|
|
6216
|
-
// rebase this origin's uncovered local writes on top. A caller that keeps a durable outbox
|
|
6217
|
-
// (meshSync, the worker replica) passes its full unacked set, so a long offline burst larger
|
|
6218
|
-
// than the in-memory `recentLocal` cap is never dropped from the rebase; without it, fall back
|
|
6219
|
-
// to the recent-local ring.
|
|
6220
6198
|
const source = pending ?? recentLocal;
|
|
6221
6199
|
const toReplay = source.filter((e) => e.version > (state.wm?.[e.origin] ?? 0));
|
|
6222
6200
|
conv.reset();
|
|
@@ -6931,6 +6909,9 @@ function storeTabSync(sig, opt, bus, injector) {
|
|
|
6931
6909
|
const { unsub, post } = bus.subscribe(opt.id, (msg) => {
|
|
6932
6910
|
if (!msg || typeof msg !== 'object')
|
|
6933
6911
|
return;
|
|
6912
|
+
// the envelope lane has its own loud guard in `receive`; join messages are fenced here
|
|
6913
|
+
if (msg.t !== 'env' && msg.proto !== OP_PROTO_VERSION)
|
|
6914
|
+
return;
|
|
6934
6915
|
switch (msg.t) {
|
|
6935
6916
|
case 'env':
|
|
6936
6917
|
if (phase === 'joining')
|
|
@@ -6947,8 +6928,8 @@ function storeTabSync(sig, opt, bus, injector) {
|
|
|
6947
6928
|
const snap = sync.snapshot();
|
|
6948
6929
|
const covered = Object.entries(snap.wm).every(([origin, v]) => (msg.wm[origin] ?? 0) >= v);
|
|
6949
6930
|
post(covered
|
|
6950
|
-
? { t: 'uptodate', to: msg.from }
|
|
6951
|
-
: { t: 'state', to: msg.from, state: snap });
|
|
6931
|
+
? { t: 'uptodate', proto: OP_PROTO_VERSION, to: msg.from }
|
|
6932
|
+
: { t: 'state', proto: OP_PROTO_VERSION, to: msg.from, state: snap });
|
|
6952
6933
|
}, Math.random() * jitterMs);
|
|
6953
6934
|
responseTimers.set(msg.from, timer);
|
|
6954
6935
|
return;
|
|
@@ -6970,7 +6951,7 @@ function storeTabSync(sig, opt, bus, injector) {
|
|
|
6970
6951
|
}
|
|
6971
6952
|
});
|
|
6972
6953
|
const unsubEnv = sync.subscribe((env) => post({ t: 'env', env }));
|
|
6973
|
-
post({ t: 'hello', from: sync.origin, wm: sync.watermark() });
|
|
6954
|
+
post({ t: 'hello', proto: OP_PROTO_VERSION, from: sync.origin, wm: sync.watermark() });
|
|
6974
6955
|
helloTimer = setTimeout(goLive, helloTimeoutMs);
|
|
6975
6956
|
injector.get(DestroyRef).onDestroy(() => {
|
|
6976
6957
|
if (helloTimer !== undefined)
|