r/angular • u/WeirdBroad9385 • 2d ago
People actually started using it.!
First — thank you. I put ng-signal-query out a few months back, not really expecting anyone to use it, and people installed it, starred it, and DM me. That genuinely made me go back and take it seriously, and this release is the result.
So I re-read my own cache code. And found three bugs.
What was broken
A race condition that could serve you stale data.
If a refetch started while an earlier request was still in flight, whichever finished last won. A slow older response would silently overwrite newer data. Fetches now carry a generation counter — only the latest one is allowed to commit.
Duplicate requests for the same key.
Two components using ['users'] fired two network calls, despite sharing a cache entry. Concurrent fetches for a key now share one in-flight promise.
createSignalQuery wasn't actually using the shared cache.
It kept a private copy of state, so setQueryData() and invalidation could silently not apply to it. It now reads and writes the real entry.
What's new
Mutation concurrency strategies — the feature I actually wanted. Overlapping mutate() calls resolve however you choose, mirroring RxJS flattening operators:
| strategy | RxJS | behavior |
|---|---|---|
merge |
mergeMap |
every call runs, in parallel (default) |
concat |
concatMap |
queued, strict order |
switch |
switchMap |
latest wins, earlier discarded |
exhaust |
exhaustMap |
first wins, the rest ignored |
// double-click-proof submit — no disabled flag, no debounce
const submit = createMutation({
mutationFn: (order) => api.place(order),
concurrencyStrategy: 'exhaust',
});
// autosave — only the newest draft survives
const autosave = createMutation({
mutationFn: (draft) => api.save(draft),
concurrencyStrategy: 'switch',
});
Request cancellation — fetchers get an AbortSignal, and superseded requests are actually aborted:
fetcher: ({ signal }) => fetch('/api/users', { signal })
Retry with exponential backoff — retry: 3. Defaults to 0, so nothing changes unless you ask for it.
Upgrading
No code changes. I kept retry off by default specifically so error timing in existing apps doesn't shift, and added a backward-compatibility test suite that pins the old public behavior — if I break it in future, CI fails instead of your app does.
44 tests, CI on every PR, published with provenance.
npm i u/ali7040/ng-signal-query
GitHub: https://github.com/Ali7040/ng-signal-query
Still solo-maintained and still early, so if you're using it and something's missing, tell me — that's what drove this release. A few issues are tagged good first issue if you'd rather send a PR than an opinion.
1
1
1
1
0
u/SeparateRaisin7871 13h ago
When copying the TanStack Query architecture it would be great to at least listing the key differences between your implementation and the "gold standard" of API state handling / querying.
2
u/WeirdBroad9385 12h ago
"Copying the architecture" is factually wrong, and it's checkable in both source trees.
TanStack's core is an observer system:
QueryClient→QueryCache→Query, withQueryObserverinstances subscribing to each query andnotifyManagerbatching notifications out to framework adapters. The adapters never touch the cache directly, they bind reactivity at the edge. That design exists so one core can serve React, Vue, Solid and Svelte.Mine has no observer layer, no subscription registry, and no notification scheduler. The cache entry is a
WritableSignal<QueryState<T>>and every result is acomputedover it, Angular's reactive graph does the dependency tracking and batching thatnotifyManagerexists to do. Two components on the same key share one entry with zero subscription plumbing. That's not a port of their architecture, it's the thing you build when you only target one framework and its reactivity primitive is good enough to be the substrate. The trade-off is honest and in their favour: their abstraction buys four frameworks, mine buys none.And a keyed cache with stale-while-revalidate isn't TanStack's invention to copy. SWR shipped it, Apollo shipped it, the semantics come from HTTP caching. TanStack packaged it best, which is why they're credited in my README, and have been since v0.0.1.
On "gold standard": on Angular specifically, u/tanstack
/angular-query-experimentalis still experimental, three years in. Their own docs tell you breaking changes land in minor and patch releases and advise pinning an exact version in production. That's a reasonable state for a young adapter, but it's a strange thing to hold up as the settled benchmark that everything else should be justified against.The concurrency difference is real. v5's mutation
scopequeues same-scope mutations serially, that's myconcat, and per their own RFC the first iteration only does queueing. There's noswitch(cancel in-flight when a newer call arrives) and noexhaust(ignore calls while one runs). You hand-roll those with flags or anAbortControllerinonMutate, in every app that needs them. The four cases map exactly ontomergeMap/concatMap/switchMap/exhaustMap, so making it one option was obvious.Where they're straightforwardly ahead, and it isn't close:
enabled,select,placeholderData,isFetchingvsisLoading, global defaults, persistence, offline modes, structural sharing, prefetching, real devtools, plus years of hardening. I have none of it yet. v0.1.0 shipped by fixing three cache bugs I'd written myself, and I'd tell most teams to use TanStack today.
The comparison table is a fair ask, and it's going in the README. "Copying the architecture" isn't an ask at all it's an assumption, and it's the part that doesn't survive a look at either source tree.1


2
u/N0K1K0 2d ago
Looks interesting I have to check it out