feat: reactive getConfig() - #25
Merged
Merged
Conversation
Lets a query make subscribe (or any config field) react to response
state surgically. The framework exposes a Notifier on the execution
context that fires after each fetch; user code wraps reads of
this.response in reactiveSignal + responseNotifier.consume() to opt
into per-value reactive tracking.
class GetPrice extends RESTQuery {
getConfig() {
const ok = reactiveSignal(() => {
this.responseNotifier.consume();
return this.response?.ok;
}).value;
return { subscribe: poll({ interval: ok ? 1000 : 5000 }) };
}
}
getConfig re-runs only when ok actually transitions, not on every
fetch. The derived signal's default Object.is equals filters
propagation; the outer resolveOptions wrapper stays clean when no
deps changed; setupSubscription sees the same subscribe ref and
short-circuits.
Framework changes:
- ctx.response stays a plain field. New ctx.responseNotifier (Notifier)
is initialized in createExecutionContext.
- RESTQueryAdapter.executeRequest writes both ctx.response and
ctx.responseNotifier.notify() after each fetch.
- this.config and this.retryConfig on QueryInstance become getters
backed by a reactiveSignal wrapping resolveOptions.
- setupSubscription ref-checks against lastSubscribeFn before tearing
down, so identical-subscribe reads no-op.
- lastSubscribeFn cleared on deactivate and on paramsDidChange so
rebuilds happen in those cases regardless of ref.
Three lifecycle tests in query-stream.test.ts converted from
getConfig() returning inline closures to static config = ..., since
their getConfig usage was incidental (the tests verify subscribe
lifecycle, not dynamic config). The static form gives a stable
subscribe ref so the new behavior's ref check naturally short-circuits.
Two new tests in poll.test.ts cover the dynamic-interval and
stop-on-error patterns end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inline arrow functions inside getConfig() lexically capture `this`, so a cached config from a previous ctx will read stale params (and other ctx fields) when invoked under a new ctx. The framework was caching the resolved-options signal across ctx changes, so a query whose params changed kept seeing the old ctx through the cached subscribe closure. Replace _resolvedSignal whenever getOrCreateExecutionContext builds a new ctx. Future reads pick up the new signal and re-call getConfig with the new ctx as `this`, so inline arrows see the new params. This removes the previous test diagnostic conversion to static config: getConfig() returning an inline arrow with this.params access now works correctly across param changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflects the post-reactive-getConfig role of the function: not a one-shot setup but a reconcile against possibly-changed config. Also drops a vestigial cast on ctx.responseNotifier in the REST adapter and refreshes the _resolvedSignal header comment to name the reactivity contract (upstream notifier, param-change replacement). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the redundant inline comment at the reassignment site (the field header already explains it). Refresh the field header to name the mechanism accurately (memoization, not closure capture) and use "such as" rather than "typically" since third-party use cases are open-ended. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the broken dynamic-poll-interval example with the canonical reactiveSignal + responseNotifier.consume() pattern, add a Notifier row to the RESTQuery instance properties table, and note in the methods table that getConfig is reactive. Also adds a minor changeset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jimmy-phantom
marked this pull request as ready for review
May 16, 2026 15:33
The prior "implementations that never read reactive state continue to work" line was only true for getConfig that doesn't read mutable execution-context state at all. Direct this.response reads (the canonical pre-existing pattern) silently return stale config under the new memoization model. Spell out the failure mode and the fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pzuraq
approved these changes
May 18, 2026
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces PR #20's per-fetch subscribe rebuild with a signalium-native reactive model.
getConfig()re-runs only when the response values it actually reads have changed, not on every fetch. For poll-like subscribers this collapses steady-state rebuilds to zero; for stateful subscribers (websockets, EventSource) it eliminates the reconnect-per-fetch cost.User-facing API
Two patterns at the read site:
this.response?.X. Read once whengetConfig()first evaluates, never re-read. Use when no reactivity is needed.reactiveSignal(() => { this.responseNotifier.consume(); return this.response?.X; }).value. Re-evaluates after every fetch (whenresponseNotifierfires).reactiveSignalfilters propagation viaObject.ison the thunk's return value, so extract a primitive (e.g.response.ok,response.status) rather than returning a fresh object or the Response itself, otherwisegetConfigre-runs on every fetch even when nothing meaningful changed.The framework adds nothing beyond
responseNotifier;consume()+ plainthis.responsereads is the user pattern. Headers and other derived values use the same shape (this.response?.headers.get('etag')inside the thunk).Framework changes
ctx.responsestays a plainResponse | undefinedfield. Newctx.responseNotifier(signaliumNotifier) initialized increateExecutionContext.RESTQueryAdapter.executeRequestwrites bothctx.response = fetchResponseandctx.responseNotifier.notify()after each fetch.this.configandthis.retryConfigonQueryInstanceare getters backed by_resolvedOptions = reactiveSignal(() => resolveOptions(ctx)). The signal's dep tracking filters propagation; cached value returned when no tracked signal changed.reconcileSubscriptionref-checks againstlastSubscribeFnbefore tearing down. Identical-subscribe reads no-op.lastSubscribeFncleared ondeactivateand onparamsDidChangeso those paths always rebuild.Test plan
poll.test.ts(honors a state-dependent interval after first fetch resolves,stops polling when getConfig() switches subscribe to undefined after an error) pass with surgical behavior verified.Footgun worth flagging
A user can write
reactiveSignal(() => this.response?.ok).valueand forget theconsume()call. The derived signal would compute once with no deps and stay cached forever, silently producing stale config. Mitigation candidates: documentation convention ("always callthis.responseNotifier.consume()at the top of areactiveSignalthunk that readsthis.response") and an optional lint rule.🤖 Generated with Claude Code