-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Avoid derived()
store updates on invalid upstream state.
#9458
Conversation
|
@mnrx is attempting to deploy a commit to the Svelte Team on Vercel. A member of the Team first needs to authorize it. |
Thank you, but this really should have been multiple PRs (or better still, an issue detailing the problem). I extracted the The harsh reality is that when a maintainer sees a PR like this, they sigh, and put it to the back of the queue. Which means that merge conflicts arise, making it even less appealing to review. And then when it is eventually brought up to date (I just merged I'm telling you this not to discourage you from contributing, but to help prevent future contributions from suffering the same fate. |
I'm going to close this, as I don't see a path to merging in its current state, but please do feel welcome to create new issues and PRs for the individual features. Please avoid making drive-by formatting changes etc if you do. |
Thank you for looking at this, Rich. I figured that this would be a challenge to merge, and so tried to document and justify the changes as best as possible. Initially, I did attempt to alter only the update logic in the existing implementation, but the mode of execution that it uses poses a problem. Critical information is not known at the right point in order to correctly decide whether or not to re-evaluate a derived store as one of its dependencies updates. Eventually, I decided this feature would be much easier to write and peer-review as a new implementation. I don't think I can create PRs for "individual features"—this implementation was built to fix the premature re-evaluation issue, and the TypeScript improvements were incidental. I will bring this PR up-to-date with Can you be more specific regarding "drive-by formatting changes"? I tried not to change anything without good reason. I think Svelte really deserves to have a sound implementation for this. I lost a few days of work to my assumption that "of course Svelte doesn't re-evaluate derived stores before their depedencies". I was surprised that previous instances of this issue were (softly) dismissed as "niche". Others have implemented third-party stores with similar goals, such as warg, but I am keen to do what it take to get this upstream. I have just filed an issue (#10376) using the "Example" section above and a reproduction. Please say if there's something else I should do. |
Hi @mnrx, I too faced issues with some of the limitations of the default stores and implemented a separate library to solve them. I haven't looked into your patch in detail, but does it solve each of the issues listed here? If you are still interested in moving forward with improving stores, I would be happy to help out. As you can see by my library (stores-mono) I have some experience with stores :-D |
I saw your libraries while looking for related issues—good work! Thank you for your interest. Regarding your list, this implementation handles all five situations correctly, in my opinion. See the example component in my issue (#10376) or the three unit tests in this PR that directly relate to example e on your list. The "This Implementation" section in this PR should provide an overview of the algorithm this implementation uses to avoid re-evaluations on invalid upstream state. My standalone TypeScript version works for my projects at the moment. I'm keen to get this or another implementation included with Svelte, but I need some assurance that the maintainers agree this is an issue that should be resolved before I spend time bringing this PR up to date. |
To add to the conversation, I see the following limitations with the current stores contract/implementation:
If we are to move forward, it would be worth seeing which issues we can get traction on (i.e. that the maintainers agree with and want resolved), and then to work out how best to divide them into discrete pull requests and in what order to avoid the issues you've faced in this PR. To give more context to the list above: # 2 Mixing svelte stores and 3rd party stores in the context of a diamond dependency # 3 Greedy triggering of changes as defined by # 4 Current typescript definitions don't fully propagate undefined writable<number>().subscribe((x: number) => console.log('I was promised a number!', x)); Fixing this is simple (I can see two clear solutions), but whether this should be fixed or not is a question better left to the current maintainers. # 5 Exception handling // run when setting the value of a store
// e.g. store.set(new_value);
if (run_queue) {
for (let i = 0; i < subscriber_queue.length; i += 2) {
subscriber_queue[i][0](subscriber_queue[i + 1]); // <-- If this throws an exception, the rest dont get run at this time
}
subscriber_queue.length = 0; // <-- this will be skipped
}
// <-- the exception propagates to a caller who is unleiky to be equipped to handle it |
Regarding a route forward, I concur. In response to your points:
|
@mnrx @WHenderson Why don’t y'all move this to the issue so it doesn’t get lost and others can see the discussion |
@mnrx , I agree with @Antonio-Bennett that these should be moved to issues to better track the discussion. I'll create issues for the others and link them as I do. |
Also: feat: Improve
svelte/store
's TypeScript type definitions.Also: docs: Better document
svelte/store
and it's TypeScript type definitions.This PR introduces a re-implementation of
svelte/store
. This implementation treats derived stores, in their capacity as a store subscriber themselves, with extra attention. This enables more accurate tracking of derived store validity and ensures that re-evaluation doesn't happen on invalid upstream state.Currently, a change to a store's value only causes stores which are immediately dependent on it to be invalidated. Svelte does not propagate invalidation downstream to subsequent derived stores, and this can cause issues when two criteria are met:
Svelte's current implementation correctly handles dependency diamonds, but such cases do not meet the second criterion; unequal path length.
Example
Consider the following dependency graph—A is a readable store, while B and C are derived stores.
Svelte's Current Implementation
In Svelte's current implementation, when the value of store A changes, stores B and C are each invalidated once. This is recorded in the
pending
variable inderived()
. Immediately afterwards, both stores B and C re-evaluate, store C using the old value of store B that it recorded in its privatevalues
array during initialisation of the store graph.Once stores B and C have changed value, they invalidate their subscribers. Store C has no subscribers to invalidate, but is invalidated itself by its dependency, store B. This causes store C to re-evaluate a second time, this time reaching the correct value.
The first re-evaluation of store C is wasted. This is arguably out of keeping with Svelte's efficiency, but is also—and more importantly—performed on invalid state. An exception could be thrown if, for instance:
In this case, the index (store B) will become out of range, the object will become
undefined
, and property access on that value will yield aTypeError
.I suspect that JavaScript's loose typing allows a lot of these premature re-evaluations to go unnoticed in many real-world applications, generating short-lived
NaN
s andundefined
s rather than throwing errors. I did encounter this issue organically, though.This Implementation
This implementation starts the process of propagating a change in a similar manner, but completes all invalidation before re-evaluating any derived stores. When the value of store A changes, stores B and C are both invalidated. They each maintain an invalidation counter which tracks how many of their direct dependencies are about to update.
Since stores B and C are each being invalidated for the first time in this update cycle, they each invalidate their own derived-store subscribers. This means that store B invalidates store C a second time. At this point, the end of the graph is reached.
Store A now begins notifying subscribers properly of the new value. This is when regular subscribers, such as Svelte components, are notified. Store B notices that its only dependency has updated, and thus re-evaluates. Meanwhile, importantly, store C only decrements its counter.
Store B updating causes it in turn to notify its subscribers, including store C. This indicates to store C that the last of its dependencies that were expected to update has updated, so it re-evaluates.
Store C is only re-evaluated after all of its direct dependencies that need to be re-evaluated have been.
Implementation Details
Symbols are used to hide private methods and values on store objects. This approach could be replaced with underscore-prefixed property names, but such properties would be enumerable and may make users more likely to use these private properties despite them not being part of a public API.
Object.getOwnPropertySymbols()
can retrieve thesymbol
s, however, so the properties are not truly private.Alternatively, it may be possible to use
Map
orWeakMap
to store a "shadow" object for each store which holds private data. Using symbols separates the publicset()
andupdate()
functions from their implementations, however, potentially simplifying the proxy-like behaviour in higher-order stores such asspring()
andtweened()
.Status
This implementation passes all but one of the tests in the Svelte test suite, including passing a handful of new unit tests that the current implementation does not. The lint and build steps pass, too.
One test, "passes optional set and update functions", must be changed in order to pass. The test relates to this implementation's handling of multiple synchronous calls to
set()
and/orupdate()
within a derived store'sderive_value()
function. This seems unlikely to affect any real-world applications, but—strictly speaking—it is a breaking change. A note has been added to the store documentation regarding this.The type signatures of all exports of
svelte/store
have been changed.readable()
,writable()
,readonly()
, andget()
are now more strict about the types of the arguments passed to them.derived()
is generally more flexible than before, but may also be more strict in some situations.The API of
get()
has also been extended in a non-breaking manner, and TypeScript definitions of all exports have been expanded and tested thoroughly (see tests/test.ts in mnrx/svelte-store, my TypeScript version of this module). Support for adding type information when using non-Svelte stores (e.g., RxJSObservable
s) is improved. New TypeScript-specific documentation has been added to better describe this interoperability.New documentation in this PR could be moved to the tutorial section if that would be more appropriate. This PR could also be made an RFC if the maintainers think it ought to be one.
Changes
svelte/store
, primarily changing the behaviour ofderived()
.get_store_value()
(get()
) to src/runtime/store/index.js, while still exporting the function from src/runtime/internal/utils.js.allow_stale
option toget()
.safe_not_equal()
.tweened()
to pass re-added unit tests.derived()
.@ts-expect-error
directives in tests/store/test.ts.ExternalReadable<T>
type to cast external stores to.derived()
overloads that make it easier to add type information to external stores.derived()
.on_start()
in more detail.allow_stale
option forget()
.ts
type for all snippets in order to show type annotations on hover.Questions
For the purpose of identifier casing, are function parameter names considered part of the public API? For instance, the
allow_stale
identifier I have added toget()
appears in documentation and will appear in auto-completion pop-ups in users' editors, but will not appear in their source code. If yes, thenallow_stale
,on_start()
,on_stop()
,initial_value
, and possibly other identifiers can be renamed.Is there a reason the unit tests for
svelte/motion
were removed in the Svelte 5 migration? Should they not be re-added?Does this impact
store_get()
,connect_store_to_signal()
, and related functions? At least the first may be possible to simplify withallow_stale
.Should
EMPTY_FUNC
not be used over inlinenoop()
? Do these have different semantics?Related Issues
I've searched open PRs and issues for anything related to
svelte/store
. These may be affected by this PR:derived
#9084Partly resolved with explicit type cast of
initial_value
, e.g.,null as unknown as number
.Partly resolved with new
derived()
overloads withExternalReadable<T>
.Partly resolved by changes to documentation and type definition comments.
Partly resolved by changes to documentation.
Partly resolved by new
allow_stale
option forget()
.Possibly resolved, or would be by exporting the symbols in this implementation.
spring()
andtweened()
.Additionally, fixes or additions could be added for the following:
A public
toObservable()
function, since the inverse is already implemented inwrap_store()
.New
current_value
andfinal_value
arguments toon_start()
andon_stop()
respectively (see Provide "what changed" array (of Booleans) to derived stores #6777 comment).Could easily be merged into this PR.
Fourth parameter for
ComplexDeriveValue<S, T>
, or replacement of second parameter with an object (like Provide current value of store in second argument of stores #7520).Licence
This JavaScript version of the store is licensed under the MIT License. The TypeScript version in my other repository is licensed under the Open Software License version 3.0. Please tell me if this poses an issue.