From 91f5b844cbfb61b47c38ee4c572946e77d2d11c6 Mon Sep 17 00:00:00 2001 From: Will Henderson Date: Sun, 11 Feb 2024 20:26:19 +1100 Subject: [PATCH 1/4] add test to exercise semi-diamond dependency problem --- packages/svelte/tests/store/test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/svelte/tests/store/test.ts b/packages/svelte/tests/store/test.ts index 9664d35701b4..82d47227386a 100644 --- a/packages/svelte/tests/store/test.ts +++ b/packages/svelte/tests/store/test.ts @@ -551,6 +551,22 @@ describe('derived', () => { }); }); }); + + it('only updates once dependents are resolved', () => { + const a = writable(1); + const b = derived(a, a => a*2); + const c = derived([a,b], ([a,b]) => a+b); + + const values: number[] = []; + + const unsubscribe = c.subscribe(c => { + values.push(c); + }); + + a.set(2); + a.set(3); + assert.deepEqual(values, [3, 6, 9]); + }); }); describe('get', () => { From f52adbcc7aa678ee8219f49a0448052f4b92af7d Mon Sep 17 00:00:00 2001 From: Will Henderson Date: Thu, 15 Feb 2024 12:02:32 +1100 Subject: [PATCH 2/4] Allow deep invalidation for stores --- packages/svelte/src/store/index.js | 35 ++++++++++++++++++++++++++- packages/svelte/src/store/public.d.ts | 6 ++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/store/index.js b/packages/svelte/src/store/index.js index 762aa4f912d2..f056a62a89ab 100644 --- a/packages/svelte/src/store/index.js +++ b/packages/svelte/src/store/index.js @@ -46,10 +46,25 @@ export function safe_not_equal(a, b) { export function writable(value, start = noop) { /** @type {import('./public').Unsubscriber | null} */ let stop = null; + let invalidated = false; /** @type {Set>} */ const subscribers = new Set(); + function invalidate() { + if (invalidated) + return; + + if (stop) { + invalidated = true; + + // immediately signal each subscriber as invalid + for (const subscriber of subscribers) { + subscriber[1](); + } + } + } + /** * @param {T} new_value * @returns {void} @@ -57,6 +72,8 @@ export function writable(value, start = noop) { function set(new_value) { if (safe_not_equal(value, new_value)) { value = new_value; + invalidated = false; + if (stop) { // store is ready const run_queue = !subscriber_queue.length; @@ -74,6 +91,20 @@ export function writable(value, start = noop) { } } + + const complex_set = Object.assign( + /** + * @param {T} new_value + * @returns {void} + */ + (new_value) => set(new_value), + { + set, + update, + invalidate + } + ) + /** * @param {import('./public').Updater} fn * @returns {void} @@ -92,7 +123,7 @@ export function writable(value, start = noop) { const subscriber = [run, invalidate]; subscribers.add(subscriber); if (subscribers.size === 1) { - stop = start(set, update) || noop; + stop = start(complex_set, update) || noop; } run(/** @type {T} */ (value)); return () => { @@ -157,6 +188,7 @@ export function derived(stores, fn, initial_value) { } const auto = fn.length < 2; return readable(initial_value, (set, update) => { + const invalidate = set.invalidate; let started = false; /** @type {T[]} */ const values = []; @@ -186,6 +218,7 @@ export function derived(stores, fn, initial_value) { }, () => { pending |= 1 << i; + invalidate(); } ) ); diff --git a/packages/svelte/src/store/public.d.ts b/packages/svelte/src/store/public.d.ts index 26f887e0e010..ccd7b950505e 100644 --- a/packages/svelte/src/store/public.d.ts +++ b/packages/svelte/src/store/public.d.ts @@ -19,7 +19,11 @@ export type Updater = (value: T) => T; * subscriber unsubscribes. */ export type StartStopNotifier = ( - set: (value: T) => void, + set: ((value: T) => void) & { + set: (value: T) => void, + update: (fn: Updater) => void, + invalidate: () => void + }, update: (fn: Updater) => void ) => void | (() => void); From c5db044bd776d3ed7aa25b55c5568f3e3a43e1cb Mon Sep 17 00:00:00 2001 From: Will Henderson Date: Tue, 20 Feb 2024 23:09:45 +1100 Subject: [PATCH 3/4] Correct type docs --- packages/svelte/src/store/public.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/store/public.d.ts b/packages/svelte/src/store/public.d.ts index ccd7b950505e..3ff24f0dc8f6 100644 --- a/packages/svelte/src/store/public.d.ts +++ b/packages/svelte/src/store/public.d.ts @@ -13,7 +13,7 @@ export type Updater = (value: T) => T; * Start and stop notification callbacks. * This function is called when the first subscriber subscribes. * - * @param {(value: T) => void} set Function that sets the value of the store. + * @param {((value: T) => void) & { set: (value: T) => void, update: (fn: Updater) => void, invalidate: () => void })} set Function that sets the value of the store. * @param {(value: Updater) => void} update Function that sets the value of the store after passing the current value to the update function. * @returns {void | (() => void)} Optionally, a cleanup function that is called when the last remaining * subscriber unsubscribes. From 4e666de87f0b3c6e5feb5d19b9caa10f367e000b Mon Sep 17 00:00:00 2001 From: Will Henderson Date: Tue, 20 Feb 2024 23:36:25 +1100 Subject: [PATCH 4/4] Add revalidation logic to cover final test case --- packages/svelte/src/store/index.js | 41 ++++++++++++++++++++++---- packages/svelte/src/store/private.d.ts | 5 +++- packages/svelte/src/store/public.d.ts | 10 ++++--- packages/svelte/src/store/utils.js | 6 ++-- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/store/index.js b/packages/svelte/src/store/index.js index f056a62a89ab..904064e70ec5 100644 --- a/packages/svelte/src/store/index.js +++ b/packages/svelte/src/store/index.js @@ -65,6 +65,20 @@ export function writable(value, start = noop) { } } + function revalidate() { + if (!invalidated) + return; + + invalidated = false; + + if (stop) { + // immediately signal each subscriber as revalidated + for (const subscriber of subscribers) { + subscriber[2](); + } + } + } + /** * @param {T} new_value * @returns {void} @@ -89,6 +103,8 @@ export function writable(value, start = noop) { } } } + else + revalidate(); } @@ -101,7 +117,8 @@ export function writable(value, start = noop) { { set, update, - invalidate + invalidate, + revalidate } ) @@ -116,11 +133,12 @@ export function writable(value, start = noop) { /** * @param {import('./public').Subscriber} run * @param {import('./private').Invalidator} [invalidate] + * @param {import('./private').Revalidator} [revalidate] * @returns {import('./public').Unsubscriber} */ - function subscribe(run, invalidate = noop) { + function subscribe(run, invalidate = noop, revalidate = noop) { /** @type {import('./private').SubscribeInvalidateTuple} */ - const subscriber = [run, invalidate]; + const subscriber = [run, invalidate, revalidate]; subscribers.add(subscriber); if (subscribers.size === 1) { stop = start(complex_set, update) || noop; @@ -188,16 +206,18 @@ export function derived(stores, fn, initial_value) { } const auto = fn.length < 2; return readable(initial_value, (set, update) => { - const invalidate = set.invalidate; + const { invalidate, revalidate } = set; let started = false; /** @type {T[]} */ const values = []; let pending = 0; + let changed = stores_array.length === 0; let cleanup = noop; const sync = () => { - if (pending) { + if (!changed || pending) { return; } + changed = false; cleanup(); const result = fn(single ? values[0] : values, set, update); if (auto) { @@ -211,6 +231,7 @@ export function derived(stores, fn, initial_value) { store, (value) => { values[i] = value; + changed = true; pending &= ~(1 << i); if (started) { sync(); @@ -219,6 +240,16 @@ export function derived(stores, fn, initial_value) { () => { pending |= 1 << i; invalidate(); + }, + () => { + pending &= ~(1 << i); + if (!changed && !pending) { + revalidate(); + } + else + if (started) { + sync(); + } } ) ); diff --git a/packages/svelte/src/store/private.d.ts b/packages/svelte/src/store/private.d.ts index f59b5843186e..1e36a8836449 100644 --- a/packages/svelte/src/store/private.d.ts +++ b/packages/svelte/src/store/private.d.ts @@ -3,8 +3,11 @@ import { Readable, Subscriber } from './public.js'; /** Cleanup logic callback. */ export type Invalidator = (value?: T) => void; +/** Cleanup logic callback. */ +export type Revalidator = (value?: T) => void; + /** Pair of subscriber and invalidator. */ -export type SubscribeInvalidateTuple = [Subscriber, Invalidator]; +export type SubscribeInvalidateTuple = [Subscriber, Invalidator, Revalidator]; /** One or more `Readable`s. */ export type Stores = diff --git a/packages/svelte/src/store/public.d.ts b/packages/svelte/src/store/public.d.ts index 3ff24f0dc8f6..e1cb134b9509 100644 --- a/packages/svelte/src/store/public.d.ts +++ b/packages/svelte/src/store/public.d.ts @@ -1,4 +1,4 @@ -import type { Invalidator } from './private.js'; +import type { Invalidator, Revalidator } from './private.js'; /** Callback to inform of a value updates. */ export type Subscriber = (value: T) => void; @@ -22,7 +22,8 @@ export type StartStopNotifier = ( set: ((value: T) => void) & { set: (value: T) => void, update: (fn: Updater) => void, - invalidate: () => void + invalidate: () => void, + revalidate: () => void }, update: (fn: Updater) => void ) => void | (() => void); @@ -32,9 +33,10 @@ export interface Readable { /** * Subscribe on value changes. * @param run subscription callback - * @param invalidate cleanup callback + * @param invalidate cleanup callback - run when inputs are in an indeterminate state + * @param revalidate cleanup callback - run when inputs have been resolved to their previous values */ - subscribe(this: void, run: Subscriber, invalidate?: Invalidator): Unsubscriber; + subscribe(this: void, run: Subscriber, invalidate?: Invalidator, revalidate?: Revalidator): Unsubscriber; } /** Writable interface for both updating and subscribing. */ diff --git a/packages/svelte/src/store/utils.js b/packages/svelte/src/store/utils.js index 9bcc914fd138..9aaed1e424cc 100644 --- a/packages/svelte/src/store/utils.js +++ b/packages/svelte/src/store/utils.js @@ -5,9 +5,10 @@ import { noop } from '../internal/common.js'; * @param {import('./public').Readable | null | undefined} store * @param {(value: T) => void} run * @param {(value: T) => void} [invalidate] + * @param {(value: T) => void} [revalidate] * @returns {() => void} */ -export function subscribe_to_store(store, run, invalidate) { +export function subscribe_to_store(store, run, invalidate, revalidate) { if (store == null) { // @ts-expect-error run(undefined); @@ -22,7 +23,8 @@ export function subscribe_to_store(store, run, invalidate) { const unsub = store.subscribe( run, // @ts-expect-error - invalidate + invalidate, + revalidate ); // Also support RxJS