@mmstack/primitives 22.9.4 → 22.10.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.
|
@@ -3589,7 +3589,7 @@ function createPointerDrag(opt) {
|
|
|
3589
3589
|
return base;
|
|
3590
3590
|
}
|
|
3591
3591
|
const hostRef = inject((ElementRef), { optional: true });
|
|
3592
|
-
const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], stopPropagation = false, debugName = 'pointerDrag', } = opt ?? {};
|
|
3592
|
+
const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], stopPropagation = false, capture = false, debugName = 'pointerDrag', } = opt ?? {};
|
|
3593
3593
|
const resolve = (t) => {
|
|
3594
3594
|
if (!t)
|
|
3595
3595
|
return null;
|
|
@@ -3721,6 +3721,7 @@ function createPointerDrag(opt) {
|
|
|
3721
3721
|
const controller = new AbortController();
|
|
3722
3722
|
el.addEventListener('pointerdown', onDown(el), {
|
|
3723
3723
|
signal: controller.signal,
|
|
3724
|
+
capture,
|
|
3724
3725
|
});
|
|
3725
3726
|
return () => {
|
|
3726
3727
|
controller.abort();
|
|
@@ -4323,7 +4324,7 @@ function generateOrigin$1() {
|
|
|
4323
4324
|
return globalThis.crypto.randomUUID();
|
|
4324
4325
|
return Math.random().toString(36).substring(2);
|
|
4325
4326
|
}
|
|
4326
|
-
const isPlainArray$
|
|
4327
|
+
const isPlainArray$2 = (v) => Array.isArray(v) && !isOpaque(v);
|
|
4327
4328
|
/**
|
|
4328
4329
|
* Reference-identity-pruned structural diff — the same short-circuit discipline as `merge3`:
|
|
4329
4330
|
* an untouched subtree kept its reference (the store's copy-on-write contract), so the walk
|
|
@@ -4348,7 +4349,7 @@ function diffNode(prev, next, path, ops) {
|
|
|
4348
4349
|
}
|
|
4349
4350
|
return;
|
|
4350
4351
|
}
|
|
4351
|
-
if (isPlainArray$
|
|
4352
|
+
if (isPlainArray$2(prev) && isPlainArray$2(next)) {
|
|
4352
4353
|
// same length → per-index descent (matches `arr[i].x.set(...)` writes); a length
|
|
4353
4354
|
// change is a whole unit — index attribution lies under insert/remove/reorder
|
|
4354
4355
|
if (prev.length === next.length) {
|
|
@@ -4367,7 +4368,7 @@ function applyAt(container, path, idx, op) {
|
|
|
4367
4368
|
const seg = path[idx];
|
|
4368
4369
|
if (seg === '__proto__')
|
|
4369
4370
|
return container;
|
|
4370
|
-
const base = isPlainArray$
|
|
4371
|
+
const base = isPlainArray$2(container)
|
|
4371
4372
|
? container.slice()
|
|
4372
4373
|
: isRecord$1(container)
|
|
4373
4374
|
? { ...container }
|
|
@@ -5064,12 +5065,15 @@ function createHlcClock(now = Date.now) {
|
|
|
5064
5065
|
}
|
|
5065
5066
|
|
|
5066
5067
|
/**
|
|
5067
|
-
* Wire protocol version. Version 2 ops carry `cites` + `epoch` (the dot-citation register)
|
|
5068
|
-
*
|
|
5069
|
-
*
|
|
5070
|
-
*
|
|
5068
|
+
* Wire protocol version. Version 2 ops carry `cites` + `epoch` (the dot-citation register).
|
|
5069
|
+
* Version 3 changes MATERIALIZATION semantics, not shape: grafts descend through plain arrays
|
|
5070
|
+
* (per-index ops materialize instead of silently dropping) and the documented drop rule is
|
|
5071
|
+
* enforced for non-plain ancestors — so a v2 replica and a v3 replica CANNOT converge on the
|
|
5072
|
+
* same op set, which is exactly what the version fence exists to make loud. Envelopes from
|
|
5073
|
+
* other versions are dropped loudly, and the tab-sync join protocol refuses to pair mismatched
|
|
5074
|
+
* peers: versions are never silently mixed, in shape OR in meaning.
|
|
5071
5075
|
*/
|
|
5072
|
-
const OP_PROTO_VERSION =
|
|
5076
|
+
const OP_PROTO_VERSION = 3;
|
|
5073
5077
|
const CONFLICT_BRAND = '~mmstackConflict';
|
|
5074
5078
|
function isConflicted(value) {
|
|
5075
5079
|
return typeof value === 'object' && value !== null && CONFLICT_BRAND in value;
|
|
@@ -5326,7 +5330,14 @@ const mergeFold = (merge) => {
|
|
|
5326
5330
|
return { kind: 'set', value: acc };
|
|
5327
5331
|
};
|
|
5328
5332
|
};
|
|
5329
|
-
const
|
|
5333
|
+
const isPlainArray$1 = (v) => Array.isArray(v) && !isOpaque(v);
|
|
5334
|
+
/**
|
|
5335
|
+
* The graft-side container admission. MUST equal `diffNode`'s descent set (plain records and
|
|
5336
|
+
* plain non-opaque arrays) and `applyAt`'s copy set: emission, incremental apply and
|
|
5337
|
+
* checkpoint materialization decide "container or leaf" identically, or a checkpoint-seeded
|
|
5338
|
+
* replica and an incrementally-applied one disagree about the same op set.
|
|
5339
|
+
*/
|
|
5340
|
+
const isContainer = (v) => isPlainArray$1(v) || isRecord$1(v);
|
|
5330
5341
|
/**
|
|
5331
5342
|
* The unsequenced-topology convergence core: a dot-citation multi-value register per path.
|
|
5332
5343
|
* An op supersedes exactly the sibling dots it cites; uncited concurrent writes stay live; a
|
|
@@ -5385,8 +5396,6 @@ function createConvergingApply(opt) {
|
|
|
5385
5396
|
}
|
|
5386
5397
|
return out.sort((a, b) => a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0);
|
|
5387
5398
|
};
|
|
5388
|
-
// The live siblings a frontier had observed: those that arrived at or below its captured seq.
|
|
5389
|
-
// Used by a fork commit so it supersedes only what it saw when it forked, not later writes.
|
|
5390
5399
|
const liveObserved = (reg, frontier) => {
|
|
5391
5400
|
const live = liveOf(reg);
|
|
5392
5401
|
if (!frontier)
|
|
@@ -5394,11 +5403,6 @@ function createConvergingApply(opt) {
|
|
|
5394
5403
|
const sm = seqs.get(keyOf$1(reg.path));
|
|
5395
5404
|
return live.filter((s) => (sm?.get(s.origin) ?? 0) <= frontier.seq);
|
|
5396
5405
|
};
|
|
5397
|
-
// JSON of a tuple array, not a separator-joined string: `origin` is a caller-supplied value on a
|
|
5398
|
-
// P2P peer, so a naive `origin@p.l#epoch` join lets a crafted origin collide the signatures of two
|
|
5399
|
-
// distinct live sets. A collision makes refresh() skip a fold update, and since that skip is
|
|
5400
|
-
// arrival-order-sensitive it breaks convergence. JSON.stringify escapes the strings and the array
|
|
5401
|
-
// structure is unambiguous, so the signature is injective in the live set.
|
|
5402
5406
|
const sigOf = (live) => JSON.stringify(live.map((s) => [s.origin, s.hlc.p, s.hlc.l, s.epoch, s.kind]));
|
|
5403
5407
|
/** Recompute the fold cache; true iff the materialized result meaningfully changed. */
|
|
5404
5408
|
const refresh = (reg) => {
|
|
@@ -5448,9 +5452,6 @@ function createConvergingApply(opt) {
|
|
|
5448
5452
|
}
|
|
5449
5453
|
return true;
|
|
5450
5454
|
};
|
|
5451
|
-
// A lone tombstone is droppable only if nothing else still materializes its key: no live
|
|
5452
|
-
// descendant register would resurface, and no live ancestor `set` value still holds it. Mirrors
|
|
5453
|
-
// the relay's retention twin so a client that prunes converges with a joiner seeded from the relay.
|
|
5454
5455
|
const tombstoneDroppable = (key, reg) => {
|
|
5455
5456
|
for (const [k, other] of registers) {
|
|
5456
5457
|
if (k === key)
|
|
@@ -5478,31 +5479,37 @@ function createConvergingApply(opt) {
|
|
|
5478
5479
|
}
|
|
5479
5480
|
return undefined;
|
|
5480
5481
|
};
|
|
5481
|
-
// graft with the deterministic type-change rule: a graft whose parent location is not a plain
|
|
5482
|
-
// record is DROPPED (the register stays intact and resurfaces if the container is restored)
|
|
5483
5482
|
const graft = (tree, rel, res) => {
|
|
5484
5483
|
if (!isContainer(tree))
|
|
5485
5484
|
return tree;
|
|
5486
5485
|
const head = String(rel[0]);
|
|
5486
|
+
if (head === '__proto__')
|
|
5487
|
+
return tree;
|
|
5488
|
+
const copyOf = () => (isPlainArray$1(tree) ? tree.slice() : { ...tree });
|
|
5487
5489
|
if (rel.length === 1) {
|
|
5488
5490
|
if (res.kind === 'delete') {
|
|
5489
5491
|
if (!Object.hasOwn(tree, head))
|
|
5490
5492
|
return tree;
|
|
5491
|
-
const copy =
|
|
5493
|
+
const copy = copyOf();
|
|
5492
5494
|
delete copy[head];
|
|
5493
5495
|
return copy;
|
|
5494
5496
|
}
|
|
5495
|
-
|
|
5497
|
+
const copy = copyOf();
|
|
5498
|
+
copy[head] = res.value;
|
|
5499
|
+
return copy;
|
|
5496
5500
|
}
|
|
5497
5501
|
if (!Object.hasOwn(tree, head)) {
|
|
5498
|
-
// vivify an absent middle container so a checkpoint-seeded materialization matches a peer that
|
|
5499
|
-
// applied the ops incrementally (incremental apply creates missing parents). A numeric next
|
|
5500
|
-
// segment vivifies an array, else an object, mirroring the incremental apply path.
|
|
5501
5502
|
const vivified = typeof rel[1] === 'number' ? [] : {};
|
|
5502
|
-
|
|
5503
|
+
const copy = copyOf();
|
|
5504
|
+
copy[head] = graft(vivified, rel.slice(1), res);
|
|
5505
|
+
return copy;
|
|
5503
5506
|
}
|
|
5504
5507
|
const child = graft(tree[head], rel.slice(1), res);
|
|
5505
|
-
|
|
5508
|
+
if (child === tree[head])
|
|
5509
|
+
return tree;
|
|
5510
|
+
const copy = copyOf();
|
|
5511
|
+
copy[head] = child;
|
|
5512
|
+
return copy;
|
|
5506
5513
|
};
|
|
5507
5514
|
/** Would a value at `rel` under `value` materialize, per the graft rules? */
|
|
5508
5515
|
const graftable = (value, rel) => {
|
|
@@ -5604,9 +5611,7 @@ function createConvergingApply(opt) {
|
|
|
5604
5611
|
const seq = ++ingestSeq;
|
|
5605
5612
|
for (const op of env.ops) {
|
|
5606
5613
|
if (o?.frontier && compareHlc(env.hlc, o.frontier) <= 0)
|
|
5607
|
-
continue;
|
|
5608
|
-
// a delete or clear at the root has no parent register to abstain to; it can only blank the
|
|
5609
|
-
// whole document, and materialize would then disagree with the delta path, so drop it
|
|
5614
|
+
continue;
|
|
5610
5615
|
if (!op.path.length && op.kind !== 'set')
|
|
5611
5616
|
continue;
|
|
5612
5617
|
const reg = regAt(op.path);
|
|
@@ -5871,15 +5876,8 @@ function opSync(source, opt) {
|
|
|
5871
5876
|
origin,
|
|
5872
5877
|
});
|
|
5873
5878
|
const subscribers = new Set();
|
|
5874
|
-
// per-origin high-watermark; `versions.get(origin)` IS the local emit counter, so a hydrate/restore
|
|
5875
|
-
// that raises our own watermark also advances the next mint — no separate counter to drift out of
|
|
5876
|
-
// sync and collide with a version acked before a reboot but dropped from a debounced outbox.
|
|
5877
5879
|
const versions = new Map();
|
|
5878
5880
|
const recentLocal = [];
|
|
5879
|
-
// highest stability frontier this peer has pruned to. A remote envelope at or below it is a settled
|
|
5880
|
-
// straggler (its state is compacted away); re-admitting one could resurrect a value below the
|
|
5881
|
-
// frontier, and per-origin version dedup cannot catch a FIRST-CONTACT straggler (no prior entry),
|
|
5882
|
-
// so the frontier is the admission gate that closes that hole on the receive path.
|
|
5883
5881
|
let prunedFrontier;
|
|
5884
5882
|
const resolvedInjector = opt.driver
|
|
5885
5883
|
? null
|
|
@@ -5887,17 +5885,6 @@ function opSync(source, opt) {
|
|
|
5887
5885
|
const log = opLog(source, opt.driver
|
|
5888
5886
|
? { origin, driver: opt.driver }
|
|
5889
5887
|
: { origin, injector: resolvedInjector });
|
|
5890
|
-
// Local envelopes stamped + registered but not yet handed to the transport. A `receive` freezes
|
|
5891
|
-
// this peer's pending writes here so it can ingest the remote WITHOUT emitting mid-receive — the
|
|
5892
|
-
// synchronous re-entrant emission that used to scramble the relay's commit order. The outbox
|
|
5893
|
-
// drains on a LATER tick, so emission always lands outside any receive callstack. Writes made
|
|
5894
|
-
// while a drain is still owed queue here too, keeping wire order == version order (else a receiver
|
|
5895
|
-
// would dedup the older, still-frozen envelope).
|
|
5896
|
-
//
|
|
5897
|
-
// Deferral rides an Angular effect, so it arms only on the injector path — the transport
|
|
5898
|
-
// topologies (`tabSync`, `meshSync`) that actually re-enter through a relay. A custom `driver`
|
|
5899
|
-
// (worker mirror, pure sim, multi-reader) owns its own scheduling and has no such re-entrancy, so
|
|
5900
|
-
// it emits synchronously; the freeze-before-observe STAMPING fix below is identical either way.
|
|
5901
5888
|
const canDefer = !opt.driver;
|
|
5902
5889
|
const outbox = [];
|
|
5903
5890
|
let receiving = false;
|
|
@@ -5983,11 +5970,6 @@ function opSync(source, opt) {
|
|
|
5983
5970
|
opt.onReject?.(env, reason);
|
|
5984
5971
|
return;
|
|
5985
5972
|
}
|
|
5986
|
-
// a settled straggler at or below the pruned stability frontier: reject it (its state is
|
|
5987
|
-
// compacted, re-admitting could resurrect a below-frontier value). All ops in an envelope share
|
|
5988
|
-
// its stamp, so the envelope hlc is the dot for every op. The live relay path never delivers a
|
|
5989
|
-
// below-frontier op (a lagging client gets a snapshot, not a delta), so this only fires on a
|
|
5990
|
-
// stray re-broadcast, e.g. over a P2P/multi-path topology.
|
|
5991
5973
|
if (prunedFrontier && compareHlc(env.hlc, prunedFrontier) <= 0)
|
|
5992
5974
|
return;
|
|
5993
5975
|
const known = versions.get(env.origin);
|
|
@@ -6060,10 +6042,6 @@ function opSync(source, opt) {
|
|
|
6060
6042
|
},
|
|
6061
6043
|
hydrate: (state, pending) => {
|
|
6062
6044
|
log.flush();
|
|
6063
|
-
// rebase this origin's uncovered local writes on top. A caller that keeps a durable outbox
|
|
6064
|
-
// (meshSync, the worker replica) passes its full unacked set, so a long offline burst larger
|
|
6065
|
-
// than the in-memory `recentLocal` cap is never dropped from the rebase; without it, fall back
|
|
6066
|
-
// to the recent-local ring.
|
|
6067
6045
|
const source = pending ?? recentLocal;
|
|
6068
6046
|
const toReplay = source.filter((e) => e.version > (state.wm?.[e.origin] ?? 0));
|
|
6069
6047
|
conv.reset();
|
|
@@ -6926,6 +6904,9 @@ function storeTabSync(sig, opt, bus, injector) {
|
|
|
6926
6904
|
const { unsub, post } = bus.subscribe(opt.id, (msg) => {
|
|
6927
6905
|
if (!msg || typeof msg !== 'object')
|
|
6928
6906
|
return;
|
|
6907
|
+
// the envelope lane has its own loud guard in `receive`; join messages are fenced here
|
|
6908
|
+
if (msg.t !== 'env' && msg.proto !== OP_PROTO_VERSION)
|
|
6909
|
+
return;
|
|
6929
6910
|
switch (msg.t) {
|
|
6930
6911
|
case 'env':
|
|
6931
6912
|
if (phase === 'joining')
|
|
@@ -6942,8 +6923,8 @@ function storeTabSync(sig, opt, bus, injector) {
|
|
|
6942
6923
|
const snap = sync.snapshot();
|
|
6943
6924
|
const covered = Object.entries(snap.wm).every(([origin, v]) => (msg.wm[origin] ?? 0) >= v);
|
|
6944
6925
|
post(covered
|
|
6945
|
-
? { t: 'uptodate', to: msg.from }
|
|
6946
|
-
: { t: 'state', to: msg.from, state: snap });
|
|
6926
|
+
? { t: 'uptodate', proto: OP_PROTO_VERSION, to: msg.from }
|
|
6927
|
+
: { t: 'state', proto: OP_PROTO_VERSION, to: msg.from, state: snap });
|
|
6947
6928
|
}, Math.random() * jitterMs);
|
|
6948
6929
|
responseTimers.set(msg.from, timer);
|
|
6949
6930
|
return;
|
|
@@ -6965,7 +6946,7 @@ function storeTabSync(sig, opt, bus, injector) {
|
|
|
6965
6946
|
}
|
|
6966
6947
|
});
|
|
6967
6948
|
const unsubEnv = sync.subscribe((env) => post({ t: 'env', env }));
|
|
6968
|
-
post({ t: 'hello', from: sync.origin, wm: sync.watermark() });
|
|
6949
|
+
post({ t: 'hello', proto: OP_PROTO_VERSION, from: sync.origin, wm: sync.watermark() });
|
|
6969
6950
|
helloTimer = setTimeout(goLive, helloTimeoutMs);
|
|
6970
6951
|
injector.get(DestroyRef).onDestroy(() => {
|
|
6971
6952
|
if (helloTimer !== undefined)
|