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 | 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 335x 335x 118x 125x 125x 125x 125x 132x 125x 125x 125x 132x 125x 125x 125x 125x 125x 125x 125x 125x 125x 125x 125x 125x 125x 125x 125x 125x 118x 335x 322x 322x 322x 322x 322x 322x 322x 335x 335x 335x 335x 335x 335x 335x 335x 335x 335x 335x 335x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 335x 335x 335x 335x 16x 16x 16x 16x 16x 16x 16x 319x 319x 319x 319x 319x 319x 319x 319x 319x 335x 319x 319x 319x 319x 335x 322x 322x 322x 322x 322x 322x 322x 322x 322x 29x 29x 322x 322x 322x 322x 322x 322x 322x 322x | /**
* @file cache.js
* @description Reusing per-unit Atlas fragments across rebuilds.
*
* `avenx serve` and `avenx watch` rebuild the whole project on every save, and
* a project's components mostly do not change between two saves. This cache
* keeps the nodes and edges each component produced so an unchanged file is
* merged back rather than re-analysed.
*
* ## What the key has to cover
*
* A component's own text is not enough. Its edges resolve `cart.total` against
* the *bridge's* declared surface, so renaming a getter must invalidate every
* consumer's fragment even though none of their files changed. The key is
* therefore the file's content plus a digest of the surfaces it resolves
* against.
*
* A stale fragment is worse than no cache — it would answer an impact query
* with relationships that no longer exist — so the key covers everything the
* fragment was derived from, and nothing is keyed on mtime or path alone.
*
* The cache lives in memory for the life of the process. It is deliberately
* not on disk: the build is not incremental anywhere else, so a disk cache
* would add invalidation risk across process boundaries for a slice of a cost
* that is already small.
* @module lib/compiler/atlas/cache
*/
import { createHash } from 'crypto';
import { AppModel } from './AppModel.js';
import { addComponentUnit } from './build.js';
/**
* How many unit fragments to retain.
*
* Large enough for a substantial application, bounded so a long-lived dev
* server cannot grow without limit.
* @type {number}
*/
const MAX_ENTRIES = 2000;
/** @type {Map<string, object>} */
const store = new Map();
/**
* Digests the surfaces a unit's analysis depends on.
*
* Only the parts a consumer can resolve against are included: a bridge's
* internals can change freely without altering what its consumers' edges mean.
* @param {Array<object>} bindings - The unit's bridge bindings.
* @param {Map<string, object>} bridges - Every bridge descriptor.
* @returns {string} A digest, stable across runs.
*/
function surfaceDigest(bindings, bridges) {
if (!bindings || bindings.length === 0) return '-';
const parts = [];
for (const binding of [...bindings].sort((a, b) => (a.local < b.local ? -1 : 1))) {
let descriptor = null;
if (bridges) {
for (const candidate of bridges.values()) {
if (candidate && candidate.name === binding.bridge) {
descriptor = candidate;
break;
}
}
}
if (!descriptor) {
parts.push(`${binding.local}=${binding.bridge}:missing`);
continue;
}
parts.push(
[
binding.local,
descriptor.name,
[...descriptor.stateKeys].sort().join(','),
[...descriptor.getters].sort().join(','),
[...descriptor.actions].sort().join(','),
[...(descriptor.events || [])].sort().join(','),
// Whether a bridge action is atomic is part of the surface a consumer
// resolves against: Rewind's write-set closure crosses this boundary.
[...(descriptor.atomicActions || [])].sort().join(','),
].join('|'),
);
}
return parts.join(';');
}
/**
* Computes the cache key for a unit.
* @param {object} unit - The unit being analysed.
* @returns {string} The key.
*/
export function cacheKey(unit) {
return createHash('sha1')
.update(unit.filePath)
.update('\0')
.update(unit.kind)
.update('\0')
.update(unit.rootDir || '')
.update('\0')
.update(unit.content)
.update('\0')
.update(surfaceDigest(unit.bridgeBindings, unit.bridges))
.digest('hex');
}
/**
* Adds a unit to the model, reusing a previous analysis when nothing it
* depends on has changed.
*
* On a miss the unit is analysed into an isolated fragment model, which is
* built leniently — a component legitimately names bridge nodes it does not
* itself declare — and then merged into the real model under its normal edge
* rules.
* @param {AppModel} model - The model being built.
* @param {object} unit - The unit, as `addComponentUnit` expects it.
* @returns {{ownerId: string, masked: string, starts: number[], cached: boolean}}
* What was added, and whether it came from the cache.
*/
export function addCachedComponentUnit(model, unit) {
const key = cacheKey(unit);
const hit = store.get(key);
if (hit) {
// Refresh recency: Map preserves insertion order, so re-inserting moves
// this entry to the back of the eviction queue.
store.delete(key);
store.set(key, hit);
model.merge(hit.fragment);
return { ownerId: hit.ownerId, masked: hit.masked, starts: hit.starts, cached: true };
}
const scratch = new AppModel({ requireNodes: false });
const result = addComponentUnit(scratch, unit);
const fragment = {
nodes: [...scratch.nodes.values()],
edges: scratch.edges,
unresolved: scratch.unresolved,
};
if (store.size >= MAX_ENTRIES) {
const oldest = store.keys().next().value;
store.delete(oldest);
}
store.set(key, { fragment, ownerId: result.ownerId, masked: result.masked, starts: result.starts });
model.merge(fragment);
return { ...result, cached: false };
}
/**
* Empties the cache.
*
* Used by tests, and available to any caller that wants a guaranteed cold
* analysis.
* @returns {void}
*/
export function clearAtlasCache() {
store.clear();
}
/**
* How many fragments are currently retained.
* @returns {number} The entry count.
*/
export function atlasCacheSize() {
return store.size;
}
export default { addCachedComponentUnit, clearAtlasCache, atlasCacheSize, cacheKey };
|