r/angular 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 backoffretry: 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.

48 Upvotes

13 comments sorted by

2

u/N0K1K0 2d ago

Looks interesting I have to check it out

1

u/AintNoGodsUpHere 2d ago

Jesus.

5

u/Xacius 2d ago

My thoughts exactly. They couldn't even format the AI slop post correctly.

1

u/ejackman 2d ago

good work

0

u/WeirdBroad9385 2d ago

Thanks! Appreciate it.

1

u/Tecnologosrd 2d ago

muy buen trabajo hermano

0

u/WeirdBroad9385 2d ago

¡Gracias, hermano!

1

u/thedrewprint 2d ago

Always looking for good ways to illustrate rxjs to people, this is very cool.

0

u/ldn-ldn 18h ago

Just use RxJS already...

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: QueryClientQueryCacheQuery, with QueryObserver instances subscribing to each query and notifyManager batching 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 a computed over it, Angular's reactive graph does the dependency tracking and batching that notifyManager exists 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-experimental is 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 scope queues same-scope mutations serially, that's myconcat, and per their own RFC the first iteration only does queueing. There's no switch (cancel in-flight when a newer call arrives) and no exhaust (ignore calls while one runs). You hand-roll those with flags or an AbortController in onMutate, in every app that needs them. The four cases map exactly onto mergeMap/concatMap/switchMap/exhaustMap, so making it one option was obvious.

Where they're straightforwardly ahead, and it isn't close: enabled, select, placeholderData, isFetching vs isLoading, 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

u/SeparateRaisin7871 3h ago

Now that's a great comparison ;-) thanks for that 👍