refactor: pass variable store explicitly to getCombined
Remove the mutable `streamFallback` global state and instead pass the `VariableStore` as an explicit argument to `getCombined` and related functions. This ensures correctness by avoiding reliance on mutable module state during reactivity calculations.
This commit is contained in:
parent
c524dd5867
commit
4c2eb65470
|
|
@ -140,9 +140,9 @@ function dispatchSet(
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = p.key;
|
const key = p.key;
|
||||||
// Use getCombined for old value so tag transitions are detected correctly
|
// Use getCombined with stream fallback for old value so tag transitions
|
||||||
// even when the variable has active mods.
|
// are detected correctly even when the variable has active mods.
|
||||||
const oldValue = getCombined(key);
|
const oldValue = getCombined(key, ctx.variables);
|
||||||
let newValue: string;
|
let newValue: string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -158,8 +158,8 @@ function dispatchSet(
|
||||||
const result = evaluateExpression(p.expr, {
|
const result = evaluateExpression(p.expr, {
|
||||||
lookup: (name: string) => {
|
lookup: (name: string) => {
|
||||||
const k = "$" + name;
|
const k = "$" + name;
|
||||||
// Use getCombined for accurate values including active mods
|
// Use getCombined with stream fallback for accurate values
|
||||||
return k === key ? undefined : getCombined(k);
|
return k === key ? undefined : getCombined(k, ctx.variables);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
newValue = String(result.value);
|
newValue = String(result.value);
|
||||||
|
|
@ -192,9 +192,9 @@ function dispatchSet(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the direct set last so cascade messages arrive first.
|
// Send the direct set last so cascade messages arrive first.
|
||||||
// Use getCombined so the stream store receives the correct value
|
// Use getCombined with stream fallback so the stream store receives the
|
||||||
// (base + active mods) rather than just the raw base.
|
// correct value (base + active mods) rather than just the raw base.
|
||||||
const combinedValue = getCombined(key);
|
const combinedValue = getCombined(key, ctx.variables);
|
||||||
const r1 = sendMessage("var", { action: "set", key, value: combinedValue });
|
const r1 = sendMessage("var", { action: "set", key, value: combinedValue });
|
||||||
const u1 = unwrap(r1);
|
const u1 = unwrap(r1);
|
||||||
if (!u1.ok) return u1;
|
if (!u1.ok) return u1;
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,9 @@
|
||||||
* Combined value = base + sum(activeMods). Tag values (starting with #)
|
* Combined value = base + sum(activeMods). Tag values (starting with #)
|
||||||
* are not numeric and don't receive mods.
|
* are not numeric and don't receive mods.
|
||||||
*
|
*
|
||||||
* Limitation: base snapshots are taken at tag activation time. If another
|
* All functions that resolve variable values accept an explicit `fallback`
|
||||||
* user changes a variable's value remotely while a local tag is active,
|
* (the stream VariableStore) rather than relying on mutable module state.
|
||||||
* the deactivation will restore the snapshot base rather than the remote
|
* This ensures correctness regardless of call order.
|
||||||
* value. In practice this is rare — GMs set bases, players toggle tags.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { VarDeclaration, TagModifier } from "./declare-parser";
|
import type { VarDeclaration, TagModifier } from "./declare-parser";
|
||||||
|
|
@ -63,9 +62,6 @@ const sourceActivations = new Map<string, Array<{ tag: string; target: string; v
|
||||||
/** Set of $vars currently being re-evaluated (cycle guard) */
|
/** Set of $vars currently being re-evaluated (cycle guard) */
|
||||||
const inFlight = new Set<string>();
|
const inFlight = new Set<string>();
|
||||||
|
|
||||||
/** Fallback store for variables not tracked locally (set by other users) */
|
|
||||||
let streamFallback: VariableStore = {};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Public API
|
// Public API
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -125,9 +121,9 @@ export function extractDependencies(expr: string): string[] {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the combined value of a variable (base + sum of active mods).
|
* Get the combined value of a variable (base + sum of active mods).
|
||||||
* Falls back to the stream store for variables not tracked locally.
|
* Falls back to the provided stream store for variables not tracked locally.
|
||||||
*/
|
*/
|
||||||
export function getCombined(key: string): string {
|
export function getCombined(key: string, fallback?: VariableStore): string {
|
||||||
const base = baseValues.get(key);
|
const base = baseValues.get(key);
|
||||||
if (base !== undefined && isTagValue(base)) {
|
if (base !== undefined && isTagValue(base)) {
|
||||||
return base; // tag values don't receive numeric mods
|
return base; // tag values don't receive numeric mods
|
||||||
|
|
@ -142,12 +138,12 @@ export function getCombined(key: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to stream store
|
// Fall back to stream store
|
||||||
const fallback = streamFallback[key];
|
const fb = fallback?.[key];
|
||||||
if (fallback !== undefined) {
|
if (fb !== undefined) {
|
||||||
if (isTagValue(fallback)) return fallback;
|
if (isTagValue(fb)) return fb;
|
||||||
const fbNum = parseFloat(fallback);
|
const fbNum = parseFloat(fb);
|
||||||
if (!isNaN(fbNum)) return String(fbNum + modSum);
|
if (!isNaN(fbNum)) return String(fbNum + modSum);
|
||||||
return fallback;
|
return fb;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No base, but has mods
|
// No base, but has mods
|
||||||
|
|
@ -191,19 +187,17 @@ export function computeCascade(
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
streamFallback = currentVars;
|
|
||||||
|
|
||||||
const results: Array<{ key: string; value: string }> = [];
|
const results: Array<{ key: string; value: string }> = [];
|
||||||
|
|
||||||
// ---- Tag activation/deactivation ----
|
// ---- Tag activation/deactivation ----
|
||||||
const newValue = getCombined(changedVar);
|
const newValue = getCombined(changedVar, currentVars);
|
||||||
const newTag = isTagValue(newValue);
|
const newTag = isTagValue(newValue);
|
||||||
const oldTag = isTagValue(oldValue);
|
const oldTag = isTagValue(oldValue);
|
||||||
|
|
||||||
if (newTag !== oldTag) {
|
if (newTag !== oldTag) {
|
||||||
// Deactivate old tag
|
// Deactivate old tag
|
||||||
if (oldTag) {
|
if (oldTag) {
|
||||||
const removed = deactivateTagFromSource(changedVar, oldTag);
|
const removed = deactivateTagFromSource(changedVar, oldTag, currentVars);
|
||||||
for (const r of removed) {
|
for (const r of removed) {
|
||||||
results.push(r);
|
results.push(r);
|
||||||
}
|
}
|
||||||
|
|
@ -211,7 +205,7 @@ export function computeCascade(
|
||||||
|
|
||||||
// Activate new tag
|
// Activate new tag
|
||||||
if (newTag) {
|
if (newTag) {
|
||||||
const added = activateTagFromSource(changedVar, newTag);
|
const added = activateTagFromSource(changedVar, newTag, currentVars);
|
||||||
for (const r of added) {
|
for (const r of added) {
|
||||||
results.push(r);
|
results.push(r);
|
||||||
}
|
}
|
||||||
|
|
@ -219,7 +213,7 @@ export function computeCascade(
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Declaration re-evaluation ----
|
// ---- Declaration re-evaluation ----
|
||||||
const reevaluated = reevaluateDependents(changedVar);
|
const reevaluated = reevaluateDependents(changedVar, currentVars);
|
||||||
for (const r of reevaluated) {
|
for (const r of reevaluated) {
|
||||||
if (!results.some((x) => x.key === r.key)) {
|
if (!results.some((x) => x.key === r.key)) {
|
||||||
results.push(r);
|
results.push(r);
|
||||||
|
|
@ -238,8 +232,6 @@ export function computeInitialValues(
|
||||||
): Array<{ key: string; value: string }> {
|
): Array<{ key: string; value: string }> {
|
||||||
if (!declExprs) return [];
|
if (!declExprs) return [];
|
||||||
|
|
||||||
streamFallback = currentVars;
|
|
||||||
|
|
||||||
const allKeys = [...declExprs.keys()];
|
const allKeys = [...declExprs.keys()];
|
||||||
const sorted = topoSortAffected(new Set(allKeys));
|
const sorted = topoSortAffected(new Set(allKeys));
|
||||||
|
|
||||||
|
|
@ -253,7 +245,7 @@ export function computeInitialValues(
|
||||||
const result = evaluateExpression(expr, {
|
const result = evaluateExpression(expr, {
|
||||||
lookup: (name: string) => {
|
lookup: (name: string) => {
|
||||||
const k = "$" + name;
|
const k = "$" + name;
|
||||||
return getCombined(k);
|
return getCombined(k, currentVars);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -263,7 +255,7 @@ export function computeInitialValues(
|
||||||
// Check if this is a tag value — if so, activate it
|
// Check if this is a tag value — if so, activate it
|
||||||
const tag = isTagValue(rawValue);
|
const tag = isTagValue(rawValue);
|
||||||
if (tag) {
|
if (tag) {
|
||||||
const added = activateTagFromSource(key, tag);
|
const added = activateTagFromSource(key, tag, currentVars);
|
||||||
for (const r of added) {
|
for (const r of added) {
|
||||||
if (!results.some((x) => x.key === r.key)) {
|
if (!results.some((x) => x.key === r.key)) {
|
||||||
results.push(r);
|
results.push(r);
|
||||||
|
|
@ -272,7 +264,7 @@ export function computeInitialValues(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always emit the combined value for this key
|
// Always emit the combined value for this key
|
||||||
const combined = getCombined(key);
|
const combined = getCombined(key, currentVars);
|
||||||
if (!results.some((x) => x.key === key)) {
|
if (!results.some((x) => x.key === key)) {
|
||||||
results.push({ key, value: combined });
|
results.push({ key, value: combined });
|
||||||
}
|
}
|
||||||
|
|
@ -298,6 +290,7 @@ function isTagValue(value: string | undefined): string | null {
|
||||||
function activateTagFromSource(
|
function activateTagFromSource(
|
||||||
source: string,
|
source: string,
|
||||||
tag: string,
|
tag: string,
|
||||||
|
fallback: VariableStore,
|
||||||
): Array<{ key: string; value: string }> {
|
): Array<{ key: string; value: string }> {
|
||||||
if (!tagModMap) return [];
|
if (!tagModMap) return [];
|
||||||
|
|
||||||
|
|
@ -312,13 +305,13 @@ function activateTagFromSource(
|
||||||
// Snapshot the target's current combined value as its base before
|
// Snapshot the target's current combined value as its base before
|
||||||
// adding the mod, so deactivation can restore it correctly.
|
// adding the mod, so deactivation can restore it correctly.
|
||||||
if (!baseValues.has(mod.target)) {
|
if (!baseValues.has(mod.target)) {
|
||||||
baseValues.set(mod.target, getCombined(mod.target));
|
baseValues.set(mod.target, getCombined(mod.target, fallback));
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = evaluateExpression(mod.expression, {
|
const result = evaluateExpression(mod.expression, {
|
||||||
lookup: (name: string) => {
|
lookup: (name: string) => {
|
||||||
const k = "$" + name;
|
const k = "$" + name;
|
||||||
return getCombined(k);
|
return getCombined(k, fallback);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -334,7 +327,7 @@ function activateTagFromSource(
|
||||||
sourceEntries.push({ tag, target: mod.target, value: result.value });
|
sourceEntries.push({ tag, target: mod.target, value: result.value });
|
||||||
|
|
||||||
// Emit new combined value for the target
|
// Emit new combined value for the target
|
||||||
const combined = getCombined(mod.target);
|
const combined = getCombined(mod.target, fallback);
|
||||||
const existing = results.findIndex((r) => r.key === mod.target);
|
const existing = results.findIndex((r) => r.key === mod.target);
|
||||||
if (existing >= 0) {
|
if (existing >= 0) {
|
||||||
results[existing] = { key: mod.target, value: combined };
|
results[existing] = { key: mod.target, value: combined };
|
||||||
|
|
@ -354,6 +347,7 @@ function activateTagFromSource(
|
||||||
function deactivateTagFromSource(
|
function deactivateTagFromSource(
|
||||||
source: string,
|
source: string,
|
||||||
_tag: string,
|
_tag: string,
|
||||||
|
fallback: VariableStore,
|
||||||
): Array<{ key: string; value: string }> {
|
): Array<{ key: string; value: string }> {
|
||||||
const entries = sourceActivations.get(source);
|
const entries = sourceActivations.get(source);
|
||||||
if (!entries) return [];
|
if (!entries) return [];
|
||||||
|
|
@ -388,7 +382,7 @@ function deactivateTagFromSource(
|
||||||
|
|
||||||
// Emit new combined values for affected targets
|
// Emit new combined values for affected targets
|
||||||
for (const target of affectedTargets) {
|
for (const target of affectedTargets) {
|
||||||
results.push({ key: target, value: getCombined(target) });
|
results.push({ key: target, value: getCombined(target, fallback) });
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|
@ -400,6 +394,7 @@ function deactivateTagFromSource(
|
||||||
|
|
||||||
function reevaluateDependents(
|
function reevaluateDependents(
|
||||||
changedVar: string,
|
changedVar: string,
|
||||||
|
fallback: VariableStore,
|
||||||
): Array<{ key: string; value: string }> {
|
): Array<{ key: string; value: string }> {
|
||||||
if (!depGraph || !declExprs) return [];
|
if (!depGraph || !declExprs) return [];
|
||||||
|
|
||||||
|
|
@ -436,21 +431,21 @@ function reevaluateDependents(
|
||||||
const result = evaluateExpression(expr, {
|
const result = evaluateExpression(expr, {
|
||||||
lookup: (name: string) => {
|
lookup: (name: string) => {
|
||||||
const k = "$" + name;
|
const k = "$" + name;
|
||||||
return getCombined(k);
|
return getCombined(k, fallback);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawValue = String(result.value);
|
const rawValue = String(result.value);
|
||||||
|
|
||||||
// Check for tag transition on this declared variable
|
// Check for tag transition on this declared variable
|
||||||
const oldCombined = getCombined(key);
|
const oldCombined = getCombined(key, fallback);
|
||||||
const oldTag = isTagValue(oldCombined);
|
const oldTag = isTagValue(oldCombined);
|
||||||
const newTag = isTagValue(rawValue);
|
const newTag = isTagValue(rawValue);
|
||||||
|
|
||||||
if (oldTag !== newTag) {
|
if (oldTag !== newTag) {
|
||||||
// Deactivate old tag
|
// Deactivate old tag
|
||||||
if (oldTag) {
|
if (oldTag) {
|
||||||
const removed = deactivateTagFromSource(key, oldTag);
|
const removed = deactivateTagFromSource(key, oldTag, fallback);
|
||||||
for (const r of removed) {
|
for (const r of removed) {
|
||||||
if (!results.some((x) => x.key === r.key)) {
|
if (!results.some((x) => x.key === r.key)) {
|
||||||
results.push(r);
|
results.push(r);
|
||||||
|
|
@ -460,7 +455,7 @@ function reevaluateDependents(
|
||||||
// Activate new tag
|
// Activate new tag
|
||||||
if (newTag) {
|
if (newTag) {
|
||||||
baseValues.set(key, rawValue);
|
baseValues.set(key, rawValue);
|
||||||
const added = activateTagFromSource(key, newTag);
|
const added = activateTagFromSource(key, newTag, fallback);
|
||||||
for (const r of added) {
|
for (const r of added) {
|
||||||
if (!results.some((x) => x.key === r.key)) {
|
if (!results.some((x) => x.key === r.key)) {
|
||||||
results.push(r);
|
results.push(r);
|
||||||
|
|
@ -473,7 +468,7 @@ function reevaluateDependents(
|
||||||
baseValues.set(key, rawValue);
|
baseValues.set(key, rawValue);
|
||||||
|
|
||||||
// Emit combined value
|
// Emit combined value
|
||||||
const combined = getCombined(key);
|
const combined = getCombined(key, fallback);
|
||||||
if (!results.some((x) => x.key === key)) {
|
if (!results.some((x) => x.key === key)) {
|
||||||
results.push({ key, value: combined });
|
results.push({ key, value: combined });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue