@mmstack/primitives 21.9.4 → 21.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.
|
@@ -3552,7 +3552,7 @@ function createPointerDrag(opt) {
|
|
|
3552
3552
|
return base;
|
|
3553
3553
|
}
|
|
3554
3554
|
const hostRef = inject((ElementRef), { optional: true });
|
|
3555
|
-
const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], stopPropagation = false, debugName = 'pointerDrag', } = opt ?? {};
|
|
3555
|
+
const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], stopPropagation = false, capture = false, debugName = 'pointerDrag', } = opt ?? {};
|
|
3556
3556
|
const resolve = (t) => {
|
|
3557
3557
|
if (!t)
|
|
3558
3558
|
return null;
|
|
@@ -3684,6 +3684,7 @@ function createPointerDrag(opt) {
|
|
|
3684
3684
|
const controller = new AbortController();
|
|
3685
3685
|
el.addEventListener('pointerdown', onDown(el), {
|
|
3686
3686
|
signal: controller.signal,
|
|
3687
|
+
capture,
|
|
3687
3688
|
});
|
|
3688
3689
|
return () => {
|
|
3689
3690
|
controller.abort();
|
|
@@ -4286,7 +4287,7 @@ function generateOrigin$1() {
|
|
|
4286
4287
|
return globalThis.crypto.randomUUID();
|
|
4287
4288
|
return Math.random().toString(36).substring(2);
|
|
4288
4289
|
}
|
|
4289
|
-
const isPlainArray$
|
|
4290
|
+
const isPlainArray$2 = (v) => Array.isArray(v) && !isOpaque(v);
|
|
4290
4291
|
/**
|
|
4291
4292
|
* Reference-identity-pruned structural diff — the same short-circuit discipline as `merge3`:
|
|
4292
4293
|
* an untouched subtree kept its reference (the store's copy-on-write contract), so the walk
|
|
@@ -4311,7 +4312,7 @@ function diffNode(prev, next, path, ops) {
|
|
|
4311
4312
|
}
|
|
4312
4313
|
return;
|
|
4313
4314
|
}
|
|
4314
|
-
if (isPlainArray$
|
|
4315
|
+
if (isPlainArray$2(prev) && isPlainArray$2(next)) {
|
|
4315
4316
|
// same length → per-index descent (matches `arr[i].x.set(...)` writes); a length
|
|
4316
4317
|
// change is a whole unit — index attribution lies under insert/remove/reorder
|
|
4317
4318
|
if (prev.length === next.length) {
|
|
@@ -4330,7 +4331,7 @@ function applyAt(container, path, idx, op) {
|
|
|
4330
4331
|
const seg = path[idx];
|
|
4331
4332
|
if (seg === '__proto__')
|
|
4332
4333
|
return container;
|
|
4333
|
-
const base = isPlainArray$
|
|
4334
|
+
const base = isPlainArray$2(container)
|
|
4334
4335
|
? container.slice()
|
|
4335
4336
|
: isRecord$1(container)
|
|
4336
4337
|
? { ...container }
|
|
@@ -5024,12 +5025,15 @@ function createHlcClock(now = Date.now) {
|
|
|
5024
5025
|
}
|
|
5025
5026
|
|
|
5026
5027
|
/**
|
|
5027
|
-
* Wire protocol version. Version 2 ops carry `cites` + `epoch` (the dot-citation register)
|
|
5028
|
-
*
|
|
5029
|
-
*
|
|
5030
|
-
*
|
|
5028
|
+
* Wire protocol version. Version 2 ops carry `cites` + `epoch` (the dot-citation register).
|
|
5029
|
+
* Version 3 changes MATERIALIZATION semantics, not shape: grafts descend through plain arrays
|
|
5030
|
+
* (per-index ops materialize instead of silently dropping) and the documented drop rule is
|
|
5031
|
+
* enforced for non-plain ancestors — so a v2 replica and a v3 replica CANNOT converge on the
|
|
5032
|
+
* same op set, which is exactly what the version fence exists to make loud. Envelopes from
|
|
5033
|
+
* other versions are dropped loudly, and the tab-sync join protocol refuses to pair mismatched
|
|
5034
|
+
* peers: versions are never silently mixed, in shape OR in meaning.
|
|
5031
5035
|
*/
|
|
5032
|
-
const OP_PROTO_VERSION =
|
|
5036
|
+
const OP_PROTO_VERSION = 3;
|
|
5033
5037
|
const CONFLICT_BRAND = '~mmstackConflict';
|
|
5034
5038
|
function isConflicted(value) {
|
|
5035
5039
|
return typeof value === 'object' && value !== null && CONFLICT_BRAND in value;
|
|
@@ -5286,7 +5290,14 @@ const mergeFold = (merge) => {
|
|
|
5286
5290
|
return { kind: 'set', value: acc };
|
|
5287
5291
|
};
|
|
5288
5292
|
};
|
|
5289
|
-
const
|
|
5293
|
+
const isPlainArray$1 = (v) => Array.isArray(v) && !isOpaque(v);
|
|
5294
|
+
/**
|
|
5295
|
+
* The graft-side container admission. MUST equal `diffNode`'s descent set (plain records and
|
|
5296
|
+
* plain non-opaque arrays) and `applyAt`'s copy set: emission, incremental apply and
|
|
5297
|
+
* checkpoint materialization decide "container or leaf" identically, or a checkpoint-seeded
|
|
5298
|
+
* replica and an incrementally-applied one disagree about the same op set.
|
|
5299
|
+
*/
|
|
5300
|
+
const isContainer = (v) => isPlainArray$1(v) || isRecord$1(v);
|
|
5290
5301
|
/**
|
|
5291
5302
|
* The unsequenced-topology convergence core: a dot-citation multi-value register per path.
|
|
5292
5303
|
* An op supersedes exactly the sibling dots it cites; uncited concurrent writes stay live; a
|
|
@@ -5345,8 +5356,6 @@ function createConvergingApply(opt) {
|
|
|
5345
5356
|
}
|
|
5346
5357
|
return out.sort((a, b) => a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0);
|
|
5347
5358
|
};
|
|
5348
|
-
// The live siblings a frontier had observed: those that arrived at or below its captured seq.
|
|
5349
|
-
// Used by a fork commit so it supersedes only what it saw when it forked, not later writes.
|
|
5350
5359
|
const liveObserved = (reg, frontier) => {
|
|
5351
5360
|
const live = liveOf(reg);
|
|
5352
5361
|
if (!frontier)
|
|
@@ -5354,11 +5363,6 @@ function createConvergingApply(opt) {
|
|
|
5354
5363
|
const sm = seqs.get(keyOf$1(reg.path));
|
|
5355
5364
|
return live.filter((s) => (sm?.get(s.origin) ?? 0) <= frontier.seq);
|
|
5356
5365
|
};
|
|
5357
|
-
// JSON of a tuple array, not a separator-joined string: `origin` is a caller-supplied value on a
|
|
5358
|
-
// P2P peer, so a naive `origin@p.l#epoch` join lets a crafted origin collide the signatures of two
|
|
5359
|
-
// distinct live sets. A collision makes refresh() skip a fold update, and since that skip is
|
|
5360
|
-
// arrival-order-sensitive it breaks convergence. JSON.stringify escapes the strings and the array
|
|
5361
|
-
// structure is unambiguous, so the signature is injective in the live set.
|
|
5362
5366
|
const sigOf = (live) => JSON.stringify(live.map((s) => [s.origin, s.hlc.p, s.hlc.l, s.epoch, s.kind]));
|
|
5363
5367
|
/** Recompute the fold cache; true iff the materialized result meaningfully changed. */
|
|
5364
5368
|
const refresh = (reg) => {
|
|
@@ -5408,9 +5412,6 @@ function createConvergingApply(opt) {
|
|
|
5408
5412
|
}
|
|
5409
5413
|
return true;
|
|
5410
5414
|
};
|
|
5411
|
-
// A lone tombstone is droppable only if nothing else still materializes its key: no live
|
|
5412
|
-
// descendant register would resurface, and no live ancestor `set` value still holds it. Mirrors
|
|
5413
|
-
// the relay's retention twin so a client that prunes converges with a joiner seeded from the relay.
|
|
5414
5415
|
const tombstoneDroppable = (key, reg) => {
|
|
5415
5416
|
for (const [k, other] of registers) {
|
|
5416
5417
|
if (k === key)
|
|
@@ -5438,31 +5439,37 @@ function createConvergingApply(opt) {
|
|
|
5438
5439
|
}
|
|
5439
5440
|
return undefined;
|
|
5440
5441
|
};
|
|
5441
|
-
// graft with the deterministic type-change rule: a graft whose parent location is not a plain
|
|
5442
|
-
// record is DROPPED (the register stays intact and resurfaces if the container is restored)
|
|
5443
5442
|
const graft = (tree, rel, res) => {
|
|
5444
5443
|
if (!isContainer(tree))
|
|
5445
5444
|
return tree;
|
|
5446
5445
|
const head = String(rel[0]);
|
|
5446
|
+
if (head === '__proto__')
|
|
5447
|
+
return tree;
|
|
5448
|
+
const copyOf = () => (isPlainArray$1(tree) ? tree.slice() : { ...tree });
|
|
5447
5449
|
if (rel.length === 1) {
|
|
5448
5450
|
if (res.kind === 'delete') {
|
|
5449
5451
|
if (!Object.hasOwn(tree, head))
|
|
5450
5452
|
return tree;
|
|
5451
|
-
const copy =
|
|
5453
|
+
const copy = copyOf();
|
|
5452
5454
|
delete copy[head];
|
|
5453
5455
|
return copy;
|
|
5454
5456
|
}
|
|
5455
|
-
|
|
5457
|
+
const copy = copyOf();
|
|
5458
|
+
copy[head] = res.value;
|
|
5459
|
+
return copy;
|
|
5456
5460
|
}
|
|
5457
5461
|
if (!Object.hasOwn(tree, head)) {
|
|
5458
|
-
// vivify an absent middle container so a checkpoint-seeded materialization matches a peer that
|
|
5459
|
-
// applied the ops incrementally (incremental apply creates missing parents). A numeric next
|
|
5460
|
-
// segment vivifies an array, else an object, mirroring the incremental apply path.
|
|
5461
5462
|
const vivified = typeof rel[1] === 'number' ? [] : {};
|
|
5462
|
-
|
|
5463
|
+
const copy = copyOf();
|
|
5464
|
+
copy[head] = graft(vivified, rel.slice(1), res);
|
|
5465
|
+
return copy;
|
|
5463
5466
|
}
|
|
5464
5467
|
const child = graft(tree[head], rel.slice(1), res);
|
|
5465
|
-
|
|
5468
|
+
if (child === tree[head])
|
|
5469
|
+
return tree;
|
|
5470
|
+
const copy = copyOf();
|
|
5471
|
+
copy[head] = child;
|
|
5472
|
+
return copy;
|
|
5466
5473
|
};
|
|
5467
5474
|
/** Would a value at `rel` under `value` materialize, per the graft rules? */
|
|
5468
5475
|
const graftable = (value, rel) => {
|
|
@@ -5564,9 +5571,7 @@ function createConvergingApply(opt) {
|
|
|
5564
5571
|
const seq = ++ingestSeq;
|
|
5565
5572
|
for (const op of env.ops) {
|
|
5566
5573
|
if (o?.frontier && compareHlc(env.hlc, o.frontier) <= 0)
|
|
5567
|
-
continue;
|
|
5568
|
-
// a delete or clear at the root has no parent register to abstain to; it can only blank the
|
|
5569
|
-
// whole document, and materialize would then disagree with the delta path, so drop it
|
|
5574
|
+
continue;
|
|
5570
5575
|
if (!op.path.length && op.kind !== 'set')
|
|
5571
5576
|
continue;
|
|
5572
5577
|
const reg = regAt(op.path);
|
|
@@ -5831,15 +5836,8 @@ function opSync(source, opt) {
|
|
|
5831
5836
|
origin,
|
|
5832
5837
|
});
|
|
5833
5838
|
const subscribers = new Set();
|
|
5834
|
-
// per-origin high-watermark; `versions.get(origin)` IS the local emit counter, so a hydrate/restore
|
|
5835
|
-
// that raises our own watermark also advances the next mint — no separate counter to drift out of
|
|
5836
|
-
// sync and collide with a version acked before a reboot but dropped from a debounced outbox.
|
|
5837
5839
|
const versions = new Map();
|
|
5838
5840
|
const recentLocal = [];
|
|
5839
|
-
// highest stability frontier this peer has pruned to. A remote envelope at or below it is a settled
|
|
5840
|
-
// straggler (its state is compacted away); re-admitting one could resurrect a value below the
|
|
5841
|
-
// frontier, and per-origin version dedup cannot catch a FIRST-CONTACT straggler (no prior entry),
|
|
5842
|
-
// so the frontier is the admission gate that closes that hole on the receive path.
|
|
5843
5841
|
let prunedFrontier;
|
|
5844
5842
|
const resolvedInjector = opt.driver
|
|
5845
5843
|
? null
|
|
@@ -5847,17 +5845,6 @@ function opSync(source, opt) {
|
|
|
5847
5845
|
const log = opLog(source, opt.driver
|
|
5848
5846
|
? { origin, driver: opt.driver }
|
|
5849
5847
|
: { origin, injector: resolvedInjector });
|
|
5850
|
-
// Local envelopes stamped + registered but not yet handed to the transport. A `receive` freezes
|
|
5851
|
-
// this peer's pending writes here so it can ingest the remote WITHOUT emitting mid-receive — the
|
|
5852
|
-
// synchronous re-entrant emission that used to scramble the relay's commit order. The outbox
|
|
5853
|
-
// drains on a LATER tick, so emission always lands outside any receive callstack. Writes made
|
|
5854
|
-
// while a drain is still owed queue here too, keeping wire order == version order (else a receiver
|
|
5855
|
-
// would dedup the older, still-frozen envelope).
|
|
5856
|
-
//
|
|
5857
|
-
// Deferral rides an Angular effect, so it arms only on the injector path — the transport
|
|
5858
|
-
// topologies (`tabSync`, `meshSync`) that actually re-enter through a relay. A custom `driver`
|
|
5859
|
-
// (worker mirror, pure sim, multi-reader) owns its own scheduling and has no such re-entrancy, so
|
|
5860
|
-
// it emits synchronously; the freeze-before-observe STAMPING fix below is identical either way.
|
|
5861
5848
|
const canDefer = !opt.driver;
|
|
5862
5849
|
const outbox = [];
|
|
5863
5850
|
let receiving = false;
|
|
@@ -5942,11 +5929,6 @@ function opSync(source, opt) {
|
|
|
5942
5929
|
opt.onReject?.(env, reason);
|
|
5943
5930
|
return;
|
|
5944
5931
|
}
|
|
5945
|
-
// a settled straggler at or below the pruned stability frontier: reject it (its state is
|
|
5946
|
-
// compacted, re-admitting could resurrect a below-frontier value). All ops in an envelope share
|
|
5947
|
-
// its stamp, so the envelope hlc is the dot for every op. The live relay path never delivers a
|
|
5948
|
-
// below-frontier op (a lagging client gets a snapshot, not a delta), so this only fires on a
|
|
5949
|
-
// stray re-broadcast, e.g. over a P2P/multi-path topology.
|
|
5950
5932
|
if (prunedFrontier && compareHlc(env.hlc, prunedFrontier) <= 0)
|
|
5951
5933
|
return;
|
|
5952
5934
|
const known = versions.get(env.origin);
|
|
@@ -6019,10 +6001,6 @@ function opSync(source, opt) {
|
|
|
6019
6001
|
},
|
|
6020
6002
|
hydrate: (state, pending) => {
|
|
6021
6003
|
log.flush();
|
|
6022
|
-
// rebase this origin's uncovered local writes on top. A caller that keeps a durable outbox
|
|
6023
|
-
// (meshSync, the worker replica) passes its full unacked set, so a long offline burst larger
|
|
6024
|
-
// than the in-memory `recentLocal` cap is never dropped from the rebase; without it, fall back
|
|
6025
|
-
// to the recent-local ring.
|
|
6026
6004
|
const source = pending ?? recentLocal;
|
|
6027
6005
|
const toReplay = source.filter((e) => e.version > (state.wm?.[e.origin] ?? 0));
|
|
6028
6006
|
conv.reset();
|
|
@@ -6883,6 +6861,9 @@ function storeTabSync(sig, opt, bus, injector) {
|
|
|
6883
6861
|
const { unsub, post } = bus.subscribe(opt.id, (msg) => {
|
|
6884
6862
|
if (!msg || typeof msg !== 'object')
|
|
6885
6863
|
return;
|
|
6864
|
+
// the envelope lane has its own loud guard in `receive`; join messages are fenced here
|
|
6865
|
+
if (msg.t !== 'env' && msg.proto !== OP_PROTO_VERSION)
|
|
6866
|
+
return;
|
|
6886
6867
|
switch (msg.t) {
|
|
6887
6868
|
case 'env':
|
|
6888
6869
|
if (phase === 'joining')
|
|
@@ -6899,8 +6880,8 @@ function storeTabSync(sig, opt, bus, injector) {
|
|
|
6899
6880
|
const snap = sync.snapshot();
|
|
6900
6881
|
const covered = Object.entries(snap.wm).every(([origin, v]) => (msg.wm[origin] ?? 0) >= v);
|
|
6901
6882
|
post(covered
|
|
6902
|
-
? { t: 'uptodate', to: msg.from }
|
|
6903
|
-
: { t: 'state', to: msg.from, state: snap });
|
|
6883
|
+
? { t: 'uptodate', proto: OP_PROTO_VERSION, to: msg.from }
|
|
6884
|
+
: { t: 'state', proto: OP_PROTO_VERSION, to: msg.from, state: snap });
|
|
6904
6885
|
}, Math.random() * jitterMs);
|
|
6905
6886
|
responseTimers.set(msg.from, timer);
|
|
6906
6887
|
return;
|
|
@@ -6922,7 +6903,7 @@ function storeTabSync(sig, opt, bus, injector) {
|
|
|
6922
6903
|
}
|
|
6923
6904
|
});
|
|
6924
6905
|
const unsubEnv = sync.subscribe((env) => post({ t: 'env', env }));
|
|
6925
|
-
post({ t: 'hello', from: sync.origin, wm: sync.watermark() });
|
|
6906
|
+
post({ t: 'hello', proto: OP_PROTO_VERSION, from: sync.origin, wm: sync.watermark() });
|
|
6926
6907
|
helloTimer = setTimeout(goLive, helloTimeoutMs);
|
|
6927
6908
|
injector.get(DestroyRef).onDestroy(() => {
|
|
6928
6909
|
if (helloTimer !== undefined)
|