Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 385x 385x 385x 385x 385x 385x 105x 105x 385x 385x 277x 277x 277x 277x 277x 277x 277x 277x 277x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 287x 287x 287x 287x 287x 287x 46x 46x 46x 46x 287x 287x 871x 385x 385x 385x 871x 84x 84x 84x 84x 84x 871x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 203x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 192x 192x 192x 192x 234x 234x 252x 252x 252x 252x 252x 863x 841x 66x 66x 233x 215x 215x 173x 192x 277x 277x 277x 277x 277x 277x 277x 277x 352x 352x 532x 532x 532x 532x 532x 532x 532x 532x 352x 352x | /**
* @file writeSet.js
* @description What an atomic action writes, and whether that answer is complete.
*
* The journal does not need this. It observes the reactive proxies, so it sees
* every write an action makes whether or not anything predicted it. What needs
* this is the *report*: the developer asking "if this rolls back, what comes
* back with it?" — and, more sharply, "what does not?"
*
* The walk is over the model Atlas already built. An action's own `writes`
* edges are the first hop; every `invokes` edge to another action contributes
* that action's write set in turn, which is how a component action that calls
* `cart.addQty()` is credited with the bridge state that call touches.
*
* ## The completeness rule
*
* A write set is **bounded** only when nothing in its closure was left
* unresolved for a reason that could be hiding a write. That flag is not
* decoration: AVX_W44 compares two write sets, and comparing an incomplete one
* would produce a warning that is a false positive and a false negative at the
* same time. Anything unbounded is excluded from that comparison and reported
* under AVX_W42 instead.
* @module lib/compiler/rewind/writeSet
*/
import { AtlasEdgeKind, AtlasNodeKind, UnresolvedReason } from '../atlas/AppModel.js';
/**
* Unresolved reasons that could be concealing a write.
*
* Deliberately the same list `atlas/diagnostics.js` blocks on, for the same
* reason: a dynamic member could be the write, a shadowed identifier means a
* body was not followed, a spread could carry a value out, and an unknown
* identifier could be the missing link. None can be ruled out.
* @type {Set<string>}
*/
const BLOCKING_REASONS = new Set([
UnresolvedReason.DYNAMIC_MEMBER,
UnresolvedReason.SHADOWED_IDENTIFIER,
UnresolvedReason.SPREAD,
UnresolvedReason.UNKNOWN_IDENTIFIER,
UnresolvedReason.DYNAMIC_INVOCATION,
UnresolvedReason.SLOT_SCOPE,
]);
/**
* How many `invokes` hops the closure follows.
*
* Deep enough for a component action calling a bridge action that calls
* another, which is as far as an Avenx application usually reaches, and
* bounded so a pathological graph cannot make a build slow. Running out of
* depth makes the write set unbounded rather than silently short.
* @type {number}
*/
export const MAX_INVOKE_DEPTH = 6;
/**
* Renders one write as the string the reports and the overlap check compare.
* @param {object} model - The model.
* @param {object} edge - A `writes` edge.
* @returns {string} A readable target, e.g. `cart.items[].qty`.
*/
function writeTarget(model, edge) {
const node = model.getNode(edge.to);
if (!node) return edge.to;
const owner = node.owner ? model.getNode(node.owner) : null;
const base = owner ? `${owner.name}.${node.name}` : node.name;
if (!edge.path) return base;
// `formatPath` yields `[].qty` for an element member, which reads as part of
// the path rather than a separate word.
return edge.path.startsWith('[') ? `${base}${edge.path}` : `${base}.${edge.path}`;
}
/**
* Computes what one action writes, following the actions it invokes.
* @param {object} model - The finished AppModel.
* @param {string} actionId - The action's node id.
* @returns {{writes: string[], bounded: boolean, reasons: Array<{reason: string, expr: string=, owner: string, loc: object=}>, depthExceeded: boolean}}
* The write set, whether it is complete, and why it is not when it is not.
*/
export function computeWriteSet(model, actionId) {
/** @type {Set<string>} */
const writes = new Set();
/** @type {Array<object>} */
const reasons = [];
const seen = new Set();
let depthExceeded = false;
/**
* Walks one action.
* @param {string} id - The action node id.
* @param {number} depth - Remaining hops.
* @returns {void}
*/
const visit = (id, depth) => {
if (seen.has(id)) return;
seen.add(id);
if (depth <= 0) {
depthExceeded = true;
return;
}
for (const entry of model.unresolvedFor(id)) {
if (BLOCKING_REASONS.has(entry.reason)) {
reasons.push(entry);
}
}
for (const edge of model.outgoing(id)) {
if (edge.kind === AtlasEdgeKind.WRITES) {
writes.add(writeTarget(model, edge));
continue;
}
if (edge.kind === AtlasEdgeKind.INVOKES) {
const target = model.getNode(edge.to);
if (target && target.kind === AtlasNodeKind.ACTION) {
visit(edge.to, depth - 1);
}
}
}
};
visit(actionId, MAX_INVOKE_DEPTH);
// A write set is only as complete as the analysis behind it. A deferred
// write — one made inside a `.then()` — is a completeness problem too: the
// journal never sees it, so it is not in the set and never will be.
const node = model.getNode(actionId);
const deferred = (node && node.deferred) || [];
return {
writes: [...writes].sort(),
bounded: reasons.length === 0 && !depthExceeded && deferred.length === 0,
reasons,
depthExceeded,
};
}
/**
* Whether one action can reach another by invoking it.
*
* Used to keep AVX_W44 off a caller and its callee. Their write sets overlap
* by construction — the caller's set *contains* the callee's — and they cannot
* interleave harmfully, because a nested transaction joins the enclosing frame
* rather than opening a second one. Warning about that pair would report the
* feature working as designed.
* @param {object} model - The finished AppModel.
* @param {string} fromId - The possible caller.
* @param {string} toId - The possible callee.
* @returns {boolean} True when `fromId` reaches `toId`.
*/
export function invokes(model, fromId, toId) {
const seen = new Set();
const queue = [fromId];
let hops = 0;
while (queue.length > 0 && hops <= MAX_INVOKE_DEPTH) {
const size = queue.length;
for (let i = 0; i < size; i++) {
const current = queue.shift();
if (current === toId && current !== fromId) return true;
if (seen.has(current)) continue;
seen.add(current);
for (const edge of model.outgoing(current)) {
if (edge.kind !== AtlasEdgeKind.INVOKES) continue;
if (edge.to === toId) return true;
queue.push(edge.to);
}
}
hops++;
}
return false;
}
/**
* Finds every atomic action in the model, with its write set.
* @param {object} model - The finished AppModel.
* @returns {Array<{node: object, label: string, writes: string[], bounded: boolean, reasons: object[], depthExceeded: boolean}>}
* One entry per atomic action, ordered by node id.
*/
export function collectAtomicActions(model) {
const results = [];
for (const node of model.nodesOfKind(AtlasNodeKind.ACTION)) {
if (!node.atomic) continue;
const owner = node.owner ? model.getNode(node.owner) : null;
results.push({
node,
label: owner ? `${owner.name}.${node.name}` : node.name,
...computeWriteSet(model, node.id),
});
}
return results;
}
|