Skip to content

Commit bc06eb3

Browse files
committed
fix: fixing asymmetric diamond dependency problem
1 parent 408509a commit bc06eb3

File tree

2 files changed

+52
-7
lines changed

2 files changed

+52
-7
lines changed

src/index.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,22 @@ describe('stores', () => {
664664
unsubscribe();
665665
});
666666

667+
it('should prevent the asymmetric diamond dependency problem', () => {
668+
const a = writable(0);
669+
const b = derived(a, (a) => `b${a}`);
670+
const c = derived([a, b], ([a, b]) => `${a}${b}`);
671+
672+
const values: string[] = [];
673+
674+
const unsubscribe = c.subscribe((value) => {
675+
values.push(value);
676+
});
677+
expect(values).toEqual(['0b0']);
678+
a.set(1);
679+
expect(values).toEqual(['0b0', '1b1']);
680+
unsubscribe();
681+
});
682+
667683
it('should be able to use destructuring', () => {
668684
const store = writable(0);
669685
const derivedStore = derived(store, (value) => {

src/index.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type SubscriberFunction<T> = (value: T) => void;
1111
export interface SubscriberObject<T> {
1212
next: SubscriberFunction<T>;
1313
invalidate: () => void;
14+
invalidated: boolean;
1415
}
1516

1617
/**
@@ -100,8 +101,12 @@ const bind = <T>(object: T | null | undefined, fnName: keyof T) => {
100101

101102
const toSubscriberObject = <T>(subscriber: Subscriber<T>): SubscriberObject<T> =>
102103
typeof subscriber === 'function'
103-
? { next: subscriber.bind(null), invalidate: noop }
104-
: { next: bind(subscriber, 'next'), invalidate: bind(subscriber, 'invalidate') };
104+
? { next: subscriber.bind(null), invalidate: noop, invalidated: false }
105+
: {
106+
next: bind(subscriber, 'next'),
107+
invalidate: bind(subscriber, 'invalidate'),
108+
invalidated: false,
109+
};
105110

106111
const returnThis = function <T>(this: T): T {
107112
return this;
@@ -164,6 +169,7 @@ export const batch = <T>(fn: () => T): T => {
164169
if (needsProcessQueue) {
165170
for (const [subscriberObject, value] of queue) {
166171
queue.delete(subscriberObject);
172+
subscriberObject.invalidated = false;
167173
subscriberObject.next(value);
168174
}
169175
willProcessQueue = false;
@@ -223,6 +229,7 @@ export function get<T>(store: SubscribableStore<T>): T {
223229
export abstract class Store<T> implements Readable<T> {
224230
private _subscribers = new Set<SubscriberObject<T>>();
225231
private _cleanupFn: null | Unsubscriber = null;
232+
private _invalidated = false;
226233

227234
/**
228235
*
@@ -242,26 +249,43 @@ export abstract class Store<T> implements Readable<T> {
242249
}
243250
}
244251

252+
/**
253+
* Notifies the store that it will receive a new value very soon.
254+
* The notification will be propagated to all derived stores
255+
* (if not done already).
256+
* Note that after calling invalidate, it is required to
257+
* call set to put the store back into a normal state.
258+
*/
259+
protected invalidate(): void {
260+
if (!this._invalidated) {
261+
this._invalidated = true;
262+
for (const subscriber of this._subscribers) {
263+
if (!subscriber.invalidated) {
264+
subscriber.invalidated = true;
265+
subscriber.invalidate();
266+
}
267+
}
268+
}
269+
}
270+
245271
/**
246272
* Replaces store's state with the provided value.
247273
* Equivalent of {@link Writable.set}, but internal to the store.
248274
*
249275
* @param value - value to be used as the new state of a store.
250276
*/
251277
protected set(value: T): void {
252-
if (notEqual(this._value, value)) {
278+
if (this._invalidated || notEqual(this._value, value)) {
253279
this._value = value;
254280
if (!this._cleanupFn) {
255281
// subscriber not yet initialized
256282
return;
257283
}
258284
batch(() => {
285+
this.invalidate();
286+
this._invalidated = false;
259287
for (const subscriber of this._subscribers) {
260-
const needInvalidate = !queue.has(subscriber);
261288
queue.set(subscriber, value);
262-
if (needInvalidate) {
263-
subscriber.invalidate();
264-
}
265289
}
266290
});
267291
}
@@ -314,6 +338,10 @@ export abstract class Store<T> implements Readable<T> {
314338
this._start();
315339
}
316340
subscriberObject.next(this._value);
341+
if (this._invalidated) {
342+
subscriberObject.invalidated = true;
343+
subscriberObject.invalidate();
344+
}
317345

318346
const unsubscribe = () => {
319347
const removed = this._subscribers.delete(subscriberObject);
@@ -515,6 +543,7 @@ export abstract class DerivedStore<
515543
},
516544
invalidate: () => {
517545
pending |= 1 << idx;
546+
this.invalidate();
518547
},
519548
})
520549
);

0 commit comments

Comments
 (0)