Severity: 🟠 High (correctness)
Location
src/store/create-store.ts:157-159 (notifySubscribers); unsubscribe splices at :352-360.
Description
$subscribe stores callbacks in a subscribers array and returns an unsubscribe that splices the callback out. notifySubscribers iterates that live array:
for (const callback of subscribers) {
callback(currentState);
}
If any callback unsubscribes itself (or another subscriber) during notification, the splice shifts indices under the for…of iterator, so the next subscriber is silently skipped for that notification cycle (and a newly-subscribed one may be double-invoked). A watcher that self-detaches on a condition is a common pattern, so this is readily triggered.
Note that $onAction already guards against exactly this with const listenerSnapshot = [...actionListeners]; — notifySubscribers is simply missing the same treatment.
Reproduction
const store = createStore({ id: 's', state: () => ({ n: 0 }) });
const unsubA = store.$subscribe(() => unsubA()); // self-detaches on first notify
const seen = [];
store.$subscribe(() => seen.push('B')); // registered after A
store.n = 1; // A runs, splices itself, B is skipped
// seen === [] (B should have fired)
Suggested fix
Snapshot before iterating, matching the $onAction pattern:
for (const callback of [...subscribers]) {
callback(currentState);
}
Filed as part of a full-codebase security & correctness audit.
Severity: 🟠 High (correctness)
Location
src/store/create-store.ts:157-159(notifySubscribers); unsubscribe splices at:352-360.Description
$subscribestores callbacks in asubscribersarray and returns an unsubscribe thatsplices the callback out.notifySubscribersiterates that live array:If any callback unsubscribes itself (or another subscriber) during notification, the
spliceshifts indices under thefor…ofiterator, so the next subscriber is silently skipped for that notification cycle (and a newly-subscribed one may be double-invoked). A watcher that self-detaches on a condition is a common pattern, so this is readily triggered.Note that
$onActionalready guards against exactly this withconst listenerSnapshot = [...actionListeners];—notifySubscribersis simply missing the same treatment.Reproduction
Suggested fix
Snapshot before iterating, matching the
$onActionpattern:Filed as part of a full-codebase security & correctness audit.