Skip to content

Open-Service: Introduce query states (loading/error/success)#35214

Open
JReinhold wants to merge 12 commits into
jeppe-cursor/693d9c70from
split/open-service-query-states
Open

Open-Service: Introduce query states (loading/error/success)#35214
JReinhold wants to merge 12 commits into
jeppe-cursor/693d9c70from
split/open-service-query-states

Conversation

@JReinhold

@JReinhold JReinhold commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Closes #

What I did

Telescoping PR on top of jeppe-cursor/693d9c70 (itself on top of split/story-docs-open-service). Set the base accordingly when reviewing.

Introduces states to the open service's subscription model so the UI can tell whether a query's data is loading, succeeded, or errored — previously a subscriber only ever received the bare value, so it could not render loading/error states. The shape is heavily inspired by TanStack Query's useQuery, adapted to our load vocabulary (our "loads" are any slow async work, not just remote fetching).

QueryState

subscribe() and useServiceQuery now surface a QueryState instead of a bare value: data plus the load lifecycle (status, loadStatus, error) and derived booleans.

type QueryState<TData> = {
  data: TData | undefined;             // last successful value ("current best effort")
  error: Error | undefined;
  status: 'pending' | 'error' | 'success';
  loadStatus: 'loading' | 'idle';
  isPending: boolean;
  isSuccess: boolean;
  isError: boolean;
  isLoading: boolean;                  // any load in flight (TanStack's isFetching)
  isInitialLoading: boolean;           // first load, no data yet (TanStack's isLoading)
  isRefreshing: boolean;               // background re-load while data is shown
};

data and status are independent: data holds the last successful value (kept across a failed re-load), while status/loadStatus/error describe the async load lifecycle.

Query runtime API: bare call → query.get()

The confusing bare query(input) call that returned synchronously and fired the load behind the scenes is removed. Reads are now an explicit, side-effect-free query.get(input).

Before

// bare call: returns synchronously AND triggers a hidden background load
const fields = service.queries.getRecordFields(input);

// subscribe receives the bare value
service.queries.getRecordFields.subscribe(input, (fields) => { /* ... */ });

After

// pure read, no implicit load
const fields = service.queries.getRecordFields.get(input);

// await the full load (this query + transitively read dependencies)
const fields = await service.queries.getRecordFields.loaded(input);

// subscribe fires the reactive load and emits a QueryState (synchronously on first call)
service.queries.getRecordFields.subscribe(input, ({ data, isLoading, isError }) => { /* ... */ });

useServiceQuery: pass the query, receive a QueryState

Before

// service instance + query name, returns the bare value
const fields = useServiceQuery(recordService, 'getRecordFields', { entryId: 'a' });

After

// pass the query object directly (types infer per query); returns a QueryState
const { data, isInitialLoading, isError } = useServiceQuery(
  recordService.queries.getRecordFields,
  { entryId: 'a' }
);

A void-input query needs no input argument (useServiceQuery(query)); the input is only positional once a selector is involved (useServiceQuery(query, undefined, selector)). The service must exist — guard at a parent and conditionally render rather than passing an absent service.

Preview-side docs hooks return QueryState

useServiceDocgen / useServiceStory (and useServiceStoryDoc / useServiceStorySnippet) now return a QueryState so docs blocks can read isInitialLoading. ArgTypes and Controls use this to render the existing ArgsTable loading skeleton, matching the manager's ControlsPanel. These hooks are reimplemented without useSyncExternalStore because the preview side must keep supporting React 16. The file is renamed useServiceDocgen.tsuse-service-docgen.ts.

self.queries type inference

Query types are now threaded through handler/load contexts, so self.queries.someQuery.get(input) infers exact input/output types without manual casts (previously self.queries was Record<string, Query<unknown, unknown>>, matching how self.commands already worked).

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

Caution

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

  1. Start the internal Storybook with the experimental docgen server enabled:
    cd code && STORYBOOK_EXPERIMENTAL_DOCGEN_SERVER=true yarn storybook:ui
  2. Open any component's Docs page (e.g. a story with autodocs). On a cold load you should briefly see the ArgsTable loading skeleton in the ArgTypes / Controls blocks before the rows populate — confirming isInitialLoading is wired through. The rows should then render normally.
  3. Open the Controls panel in the manager for the same component and confirm it populates and stays correct when switching stories (it now reflects the query's load lifecycle).
  4. Open the open-service sync demo panels and confirm subscriptions still update live (values change, no console errors about destructuring/get is not a function).
  5. Spot-check a production sandbox:
    yarn task sandbox --template react-vite/default-ts --prod --no-link --start-from auto
    Open a Docs page and confirm ArgTypes/Controls render (with a brief skeleton on first load) and the Source/Description blocks still show content.

Areas most worth extra scrutiny: the synchronous first emission of subscribe(), retention of the last successful data across a failed re-load, and the React 16-compatible preview hooks (no useSyncExternalStore).

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Declare whether manual QA will be needed for this PR during the next release, through qa:needed or qa:skip

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

Release Notes

  • New Features
    • Query subscriptions now provide structured query-state objects (including data) for more consistent UI updates.
  • Documentation
    • Updated Open Service API docs to clarify the get/loaded/subscribe lifecycle and query-state model.
    • Improved docs-block behavior when the experimental docgen server is enabled.
  • Bug Fixes
    • Docs blocks now show proper loading skeletons during initial docgen retrieval.
  • Tests
    • Updated service and UI tests/mocks to match the new query-state shape and .get() access patterns.

JReinhold and others added 5 commits June 18, 2026 15:56
Introduce QueryState (data plus the load lifecycle: status/loadStatus/error and
derived booleans, inspired by TanStack Query) as the single value emitted by a
subscription. Replace the bare auto-loading `query(input)` call with a
synchronous `query.get(input)` that performs no implicit load, and make
`subscribe()` emit a QueryState synchronously on first call. Thread per-service
query types into handler/load contexts so `self.queries` infers exact types
without manual casts.

Co-authored-by: Cursor <cursoragent@cursor.com>
…directly

The hook now accepts the Query object (e.g. `myService.queries.getThing`) so
input/output types infer per query, and returns the full QueryState so consumers
can read `isLoading`, `isError`, etc. The first render is seeded synchronously
from `query.get()` wrapped in a pending state, then the real lifecycle arrives
from the subscription's first emission. Update the public exports accordingly.

Co-authored-by: Cursor <cursoragent@cursor.com>
The preview-side docs hooks (useServiceDocgen, useServiceStory and wrappers)
return QueryState and are reimplemented without useSyncExternalStore so they keep
working on React 16. ArgTypes/Controls render the ArgsTable loading skeleton via
isInitialLoading, and the manager ControlsPanel reflects the load lifecycle.
Rename useServiceDocgen.ts to use-service-docgen.ts.

Co-authored-by: Cursor <cursoragent@cursor.com>
Update change-detection, the docgen server, the sync-test demo services/stories
and the internal debug service to read via `query.get()` and to consume the
QueryState payload now emitted by `subscribe()`.

Co-authored-by: Cursor <cursoragent@cursor.com>
Migrate unit and type tests to the QueryState API and add coverage for the
synchronous first emission, error / last-data retention, reactive dependency
warming and `self.queries` type inference. Update the open-service README to
document `query.get()`, `subscribe()`'s QueryState payload and the hook shape.

Co-authored-by: Cursor <cursoragent@cursor.com>
@JReinhold JReinhold added maintenance User-facing maintenance tasks ci:normal Run our default set of CI jobs (choose this for most PRs). qa:skip Pull Requests that do not need any QA. labels Jun 18, 2026
@JReinhold JReinhold self-assigned this Jun 18, 2026
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

This PR targets jeppe-cursor/693d9c70. The default branch for contributions is next. Please make sure you are targeting the correct branch.

Generated by 🚫 dangerJS against 0cc5e6c

@storybook-app-bot

storybook-app-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

Package Benchmarks

Commit: 0cc5e6c, ran on 18 June 2026 at 21:09:23 UTC

The following packages have significant changes to their size or dependencies:

storybook

Before After Difference
Dependency count 72 72 0
Self size 21.11 MB 21.19 MB 🚨 +74 KB 🚨
Dependency size 36.42 MB 36.42 MB 0 B
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 204 204 0
Self size 802 KB 802 KB 0 B
Dependency size 91.46 MB 91.54 MB 🚨 +74 KB 🚨
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 197 197 0
Self size 32 KB 32 KB 🚨 +36 B 🚨
Dependency size 89.95 MB 90.03 MB 🚨 +74 KB 🚨
Bundle Size Analyzer Link Link

create-storybook

Before After Difference
Dependency count 73 73 0
Self size 1.08 MB 1.08 MB 0 B
Dependency size 57.54 MB 57.61 MB 🚨 +74 KB 🚨
Bundle Size Analyzer node node

@JReinhold

Copy link
Copy Markdown
Contributor Author

CI note for the reviewer (re: Chromatic): The full test suite is green. The only failing checks are the two Chromatic visual checks on storybook-ui (UI Tests/UI Review), which report visual diffs awaiting approval rather than test failures.

These diffs are expected — this PR intentionally changes those UI surfaces: the manager ControlsPanel and the addon-docs blocks (ArgTypes, Controls) now render the ArgsTable loading skeleton while a query is in its initial load. They just need the new baselines accepted in Chromatic.

All other sandboxes' Chromatic UI Tests are "unchanged".

…ries

The Description/Source service stories mock `core/story-docs` but never
enabled the `experimentalDocgenServer` feature that gates the service path
in the blocks. The flag is off by default in production builds (Chromatic)
and a plain `yarn storybook:ui`, so the mocked data was never rendered and
the stories failed visually. Enable the feature per-story in `beforeEach`
(restored on cleanup), mirroring the existing `global.FEATURES` pattern.

Co-authored-by: Cursor <cursoragent@cursor.com>
@JReinhold JReinhold marked this pull request as ready for review June 18, 2026 18:55
@JReinhold JReinhold mentioned this pull request Jun 18, 2026
54 tasks
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c36ba28f-60db-4192-9993-df83f1193230

📥 Commits

Reviewing files that changed from the base of the PR and between 24e0ad6 and 0cc5e6c.

📒 Files selected for processing (2)
  • code/addons/docs/src/blocks/blocks/use-query-subscription.ts
  • code/core/src/shared/open-service/query-runtime.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • code/addons/docs/src/blocks/blocks/use-query-subscription.ts
  • code/core/src/shared/open-service/query-runtime.ts

📝 Walkthrough

Walkthrough

The Open Service query API changes from callable functions to a method-based interface: query.get(input) for synchronous reads, query.loaded(input) for awaited loads, and query.subscribe(...) for reactive observation. Subscriptions now emit a QueryState object (containing data, status, loadStatus, error, and derived flags) synchronously on first call. useServiceQuery is refactored to accept a query directly and return QueryState. All downstream consumers—docs blocks, ControlsPanel, server services, and sync-test demos—are updated to handle { data } destructuring in subscription callbacks and use .get(...) for synchronous reads.

Changes

Open Service Query API Overhaul

Layer / File(s) Summary
QueryState types, Query method surface, and context generics
code/core/src/shared/open-service/types.ts, code/core/src/shared/open-service/query-state.ts, code/core/src/shared/open-service/index.ts, code/core/src/shared/open-service/service-definition.ts
Adds QueryStatus, LoadStatus, QueryState<TData>, introduces buildQueryState/seedQueryState/toError helpers, changes Query<TInput,TOutput> to {get, loaded, subscribe} method interface, threads TQueries generic through QuerySelf, LoadSelf, CommandSelf, and all handler context types, updates service-definition.ts to pass query schema maps into DefinedCommands, and re-exports new types from entrypoint.
Query runtime engine: .get() execution, .loaded() drain, subscribeToQuery with synchronous QueryState
code/core/src/shared/open-service/query-runtime.ts
Adds new query-runtime.ts implementing deterministic load dedup (makeLoadKey, stableHash, inFlightLoads), load triggering with cycle detection (triggerLoad, runLoadBody), .loaded() drain discovery loop (runLoaded, runReactiveLoad), synchronous-first subscribeToQuery with per-subscription lifecycle signals and deep-equality dedup, reactive load query warming (buildReactiveLoadQueries, buildLoadWrappedQueries), and query construction (createDefaultQuery, buildQueries).
Service runtime refactoring: imports and reactive-load wiring
code/core/src/shared/open-service/service-runtime.ts
Refactors service-runtime.ts to import query-runtime utilities, removes in-file query implementation (load session, dedup, validation, subscription wiring), removes local buildQueries, introduces and populates reactiveLoadQueries after defaultQueries, extends refs with reactiveLoadQueries, and updates documentation describing .get() as synchronous non-loading read.
useServiceQuery refactor and useQuerySubscription hook
code/core/src/shared/open-service/use-service-query.ts, code/addons/docs/src/blocks/blocks/use-query-subscription.ts
Replaces useServiceQuery(service, queryName, input?) overloads with (query, input?, selector?) returning QueryState; seeds synchronously via seedQueryState on render and query/input/selector identity change; subscribes via useSyncExternalStore with ref-based snapshot caching. Adds new useQuerySubscription helper for React 16/17-safe subscription with cache-key stabilization and effect-driven lifecycle.
Service runtime and subscription lifecycle tests
code/core/src/shared/open-service/service-runtime.test.ts
Introduces collectData helper for deduped data assertions; adds query state lifecycle describe block asserting status/loadStatus transitions and derived flags; replaces thrown-error assertions with error QueryState checks; adds reactive dependency warming test; switches all reads to .get(...).
Docs-block service hooks: useServiceDocgen, useServiceStory, useDocgenServiceRows
code/addons/docs/src/blocks/blocks/use-service-docgen.ts, code/addons/docs/src/blocks/blocks/use-service-story-docs.ts, code/addons/docs/src/blocks/blocks/argTypesShared.ts
Creates useServiceDocgen hook subscribing to core/docgen service. Refactors useServiceStory from useSyncExternalStore to useQuerySubscription wrapper requiring storyId: string and returning QueryState. Updates useDocgenServiceRows to require componentId, return { rows, isInitialLoading }, and destructure isInitialLoading from useServiceDocgen. Updates convenience hooks to return QueryState wrappers.
Docs blocks: ArgTypes, Controls, Description, Source with loading states and feature flag
code/addons/docs/src/blocks/blocks/ArgTypes.tsx, code/addons/docs/src/blocks/blocks/Controls.tsx, code/addons/docs/src/blocks/blocks/Description.tsx, code/addons/docs/src/blocks/blocks/Source.tsx, code/addons/docs/src/blocks/blocks/Description.stories.tsx, code/addons/docs/src/blocks/blocks/Source.stories.tsx
ArgTypes tightens componentId to required. Controls and ArgTypes render PureArgsTable with isLoading while isInitialLoading is true. Description refactors into DescriptionBody and service wrapper components with experimentalDocgenServer branching. Source accepts serviceSnippet parameter with SourceWithStoryDocsSnippet wrapper. Story fixtures toggle feature flag during setup/teardown.
ControlsPanel: useServiceQuery-based docgen fetch with isInitialLoading
code/core/src/controls/components/ControlsPanel.tsx, code/core/src/controls/components/ControlsPanel.stories.tsx
Removes docgenReady state and its useEffect; replaces with useServiceQuery(docgenService.queries.getDocgen, { id }) returning isInitialLoading; updates loading condition from docgenReady to isInitialLoading; story mock emits buildQueryState-wrapped QueryState objects.
Server subscription consumers: change-detection, module-graph, docgen service refresh
code/core/src/core-server/change-detection/change-detection-service.ts, code/core/src/core-server/change-detection/change-detection.test-helpers.ts, code/core/src/shared/open-service/services/docgen/server.ts, code/core/src/shared/open-service/services/module-graph/server.test.ts
Updates subscription callbacks to destructure { data } with guards; test helpers wrap emitted values in QueryState via toSuccessState and expose explicit get/loaded/subscribe objects on mocked queries; docgen service refactors extraction refresh to use query-based presence checks; module-graph tests handle { data } callback destructuring.
Broad .get(...) call-site updates: tests, fixtures, and type assertions
code/core/src/shared/open-service/service-registration.test.ts, code/core/src/shared/open-service/service-registration-sync.test.ts, code/core/src/shared/open-service/service-transport-leaf.test.ts, code/core/src/shared/open-service/service-validation.test.ts, code/core/src/shared/open-service/services/*/server.test.ts, code/core/src/shared/open-service/static-fetch.test.ts, code/core/src/shared/open-service/index.test-d.ts, code/core/src/shared/open-service/server.test-d.ts, code/core/src/shared/open-service/use-service-command.test.tsx, code/core/src/shared/open-service/fixtures.ts
Mechanically updates test assertions from query(...) to query.get(...) across all service test suites; updates type assertion tests to target .get handles; updates module-graph subscriptions to handle { data } callbacks; removes unused vi import.
Sync-test demos, debug service, and documentation updates
code/.storybook/open-service-debug-service.ts, code/core/src/shared/open-service/sync-test/*/, code/core/src/shared/open-service/sync-test/addon/panels/OpenServiceDemoPanel.tsx, code/core/src/shared/open-service/README.md, code/core/src/shared/open-service/manager.ts, code/core/src/shared/open-service/preview.ts
Updates debug service and sync-test stories/servers to use .get() and handle { data } destructuring in callbacks. Refactors demo panel to useServiceQuery(service.queries.*) with data/error handling. Extensively rewrites README sections on query contract, QueryState, subscription flow, and useServiceQuery usage; updates inline examples in manager and preview docs.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • storybookjs/storybook#35169: Introduces the core/story-docs service whose subscription contract and QueryState handling are consumed by use-service-story-docs.ts in this PR.
  • storybookjs/storybook#34932: Both PRs update code/.storybook/open-service-debug-service.ts to align the debug service with the new query API shape using .get() and { data } callback destructuring.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
code/addons/docs/src/blocks/blocks/Source.tsx (1)

179-225: 💤 Low value

Minor: story resolution logic is duplicated between SourceWithStorySnippet and useSourceProps.

Lines 200-210 mirror lines 116-128 — both resolve the story from of via docsContext.resolveOf. The duplication is a reasonable trade-off to lift the feature-flag decision to the component boundary (avoiding conditional hooks), and useMemo prevents redundant computation. Consider extracting a shared useResolvedStory(of, docsContext) hook if this pattern expands to more components.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/addons/docs/src/blocks/blocks/Source.tsx` around lines 179 - 225, The
story resolution logic that handles the of prop and fallback to the current
story (using docsContext.resolveOf and docsContext.storyById) is duplicated
between the SourceWithStorySnippet component's useMemo block and the
useSourceProps hook. Extract this logic into a shared custom hook named
useResolvedStory that accepts of and docsContext as parameters and returns the
resolved story, then use this hook in both the useMemo within
SourceWithStorySnippet and in useSourceProps to eliminate the duplication while
maintaining the current behavior and memoization benefits.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@code/addons/docs/src/blocks/blocks/use-service-docgen.ts`:
- Around line 34-35: The condition at line 34 comparing cache.current?.id !== id
can evaluate to false when both are undefined, causing the cache initialization
within the if block to be skipped on the first render. This leads to
cache.current.state being accessed at line 45 before the cache has been
initialized. Fix this by explicitly ensuring cache.current is initialized before
line 45 where cache.current.state is accessed. Add an explicit check before the
state access to initialize cache.current with seedQueryState if it hasn't been
initialized yet, or restructure the condition at line 34 to guarantee
initialization occurs regardless of whether id is undefined.

In `@code/core/src/shared/open-service/service-runtime.ts`:
- Around line 679-682: The triggerLoad function call in the queryDef.load block
is using rethrowAsync in the catch handler, which rethrows dependency-load
failures onto the global microtask queue and can cause uncaught global errors
instead of keeping errors contained within the query lifecycle state. Remove the
.catch(rethrowAsync) handler from the triggerLoad call to prevent
dependency-warm failures from escaping the query lifecycle and becoming global
uncaught errors.

---

Nitpick comments:
In `@code/addons/docs/src/blocks/blocks/Source.tsx`:
- Around line 179-225: The story resolution logic that handles the of prop and
fallback to the current story (using docsContext.resolveOf and
docsContext.storyById) is duplicated between the SourceWithStorySnippet
component's useMemo block and the useSourceProps hook. Extract this logic into a
shared custom hook named useResolvedStory that accepts of and docsContext as
parameters and returns the resolved story, then use this hook in both the
useMemo within SourceWithStorySnippet and in useSourceProps to eliminate the
duplication while maintaining the current behavior and memoization benefits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5f677b66-28ef-4544-a36a-d06167d09d8c

📥 Commits

Reviewing files that changed from the base of the PR and between f2a596c and 36af0d7.

📒 Files selected for processing (47)
  • code/.storybook/open-service-debug-service.ts
  • code/addons/docs/src/blocks/blocks/ArgTypes.tsx
  • code/addons/docs/src/blocks/blocks/Controls.tsx
  • code/addons/docs/src/blocks/blocks/Description.stories.tsx
  • code/addons/docs/src/blocks/blocks/Description.tsx
  • code/addons/docs/src/blocks/blocks/Source.stories.tsx
  • code/addons/docs/src/blocks/blocks/Source.tsx
  • code/addons/docs/src/blocks/blocks/argTypesShared.ts
  • code/addons/docs/src/blocks/blocks/use-service-docgen.ts
  • code/addons/docs/src/blocks/blocks/use-service-story-docs.ts
  • code/addons/docs/src/blocks/blocks/useServiceDocgen.ts
  • code/core/src/controls/components/ControlsPanel.stories.tsx
  • code/core/src/controls/components/ControlsPanel.tsx
  • code/core/src/core-server/change-detection/change-detection-service.ts
  • code/core/src/core-server/change-detection/change-detection.test-helpers.ts
  • code/core/src/shared/open-service/README.md
  • code/core/src/shared/open-service/fixtures.ts
  • code/core/src/shared/open-service/index.test-d.ts
  • code/core/src/shared/open-service/index.ts
  • code/core/src/shared/open-service/manager.ts
  • code/core/src/shared/open-service/preview.ts
  • code/core/src/shared/open-service/query-state.ts
  • code/core/src/shared/open-service/server.test-d.ts
  • code/core/src/shared/open-service/server.test.ts
  • code/core/src/shared/open-service/service-command-transport.test.ts
  • code/core/src/shared/open-service/service-definition.ts
  • code/core/src/shared/open-service/service-registration-sync.test.ts
  • code/core/src/shared/open-service/service-registration.test.ts
  • code/core/src/shared/open-service/service-runtime.test.ts
  • code/core/src/shared/open-service/service-runtime.ts
  • code/core/src/shared/open-service/service-transport-leaf.test.ts
  • code/core/src/shared/open-service/service-validation.test.ts
  • code/core/src/shared/open-service/services/docgen/server.test.ts
  • code/core/src/shared/open-service/services/docgen/server.ts
  • code/core/src/shared/open-service/services/module-graph/server.test.ts
  • code/core/src/shared/open-service/services/story-docs/server.test.ts
  • code/core/src/shared/open-service/static-fetch.test.ts
  • code/core/src/shared/open-service/sync-test/addon/panels/OpenServiceDemoPanel.tsx
  • code/core/src/shared/open-service/sync-test/local-command/local-command.stories.tsx
  • code/core/src/shared/open-service/sync-test/local-command/server.ts
  • code/core/src/shared/open-service/sync-test/remote-command/remote-command.stories.tsx
  • code/core/src/shared/open-service/sync-test/remote-command/server.ts
  • code/core/src/shared/open-service/sync-test/static-load/static-load.stories.tsx
  • code/core/src/shared/open-service/types.ts
  • code/core/src/shared/open-service/use-service-command.test.tsx
  • code/core/src/shared/open-service/use-service-query.test.tsx
  • code/core/src/shared/open-service/use-service-query.ts
💤 Files with no reviewable changes (1)
  • code/addons/docs/src/blocks/blocks/useServiceDocgen.ts

Comment thread code/addons/docs/src/blocks/blocks/use-service-docgen.ts Outdated
Comment thread code/core/src/shared/open-service/service-runtime.ts Outdated
JReinhold and others added 3 commits June 18, 2026 21:58
Unlike TanStack Query, subscriptions here can attach to queries whose data is
already cached in service state. Require data === undefined in buildQueryState
so consumers do not flash initial-load skeletons over already-available data.

Co-authored-by: Cursor <cursoragent@cursor.com>
Move query reads, loads, the drain loop, and subscriptions into query-runtime.ts
so service-runtime.ts stays focused on runtime assembly. Collapse the three
duplicate get() implementations into createQueryGet with per-surface load policies.

Co-authored-by: Cursor <cursoragent@cursor.com>
Share the preview-side subscription seed/subscribe logic between
useServiceDocgen and useServiceStory. Call getService directly in render
instead of wrapping it in useMemo.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@code/addons/docs/src/blocks/blocks/use-query-subscription.ts`:
- Around line 18-20: The effect hook in useQuerySubscription has an incomplete
dependency list that does not include the selector parameter, causing stale
subscriptions when the selector function prop changes. Add selector to the
effect's dependency array (currently containing query and cacheKey) and ensure
the cache key computation also tracks selector identity to guarantee
re-subscription occurs whenever the selector changes, preventing stale selected
state from persisting when boundSelector or similar selector references are
updated.

In `@code/addons/docs/src/blocks/blocks/use-service-story-docs.ts`:
- Around line 41-48: The useQuerySubscription call in this function only uses
storyId as the cache key, but doesn't account for changes to the selector
parameter. When the selector changes for the same story, useQuerySubscription
does not re-seed or re-subscribe because the cache key remains unchanged,
resulting in stale QueryState.data being returned. Fix this by incorporating the
selector identity (such as a reference to boundSelector or a computed hash of
it) into the cache key passed to useQuerySubscription alongside storyId, so that
selector changes trigger proper re-subscription.

In `@code/core/src/shared/open-service/query-runtime.ts`:
- Around line 944-948: The epoch value is not being invalidated during cleanup,
allowing existing isCurrent() closures to remain true and potentially execute
stale commands after unsubscribe. In the loadTeardown effect, increment the
epoch variable during the cleanup phase (when the effect is torn down) to ensure
all pending operations with old epoch values will immediately return false from
isCurrent() checks, preventing stale state writes after unsubscribe. This
applies to both occurrences of this pattern referenced in the review comment.
- Around line 92-114: The stableHash function has a collision vulnerability
where both undefined values and the literal string "__undefined__" produce
identical hash outputs, causing unrelated data loads to share promises. Fix this
by making the replacement value for undefined values collision-resistant in the
JSON.stringify replacer function within stableHash. Instead of replacing
undefined with the string "__undefined__", use a marker approach that ensures
undefined values cannot collide with any other input (such as wrapping in an
object with a type indicator or using a sufficiently unique string marker that
cannot occur naturally from other inputs). This ensures makeLoadKey generates
unique keys for different validatedInput values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29d93cb6-d541-4f9c-bc40-698a661d0e18

📥 Commits

Reviewing files that changed from the base of the PR and between 36af0d7 and efefbe4.

📒 Files selected for processing (9)
  • code/addons/docs/src/blocks/blocks/use-query-subscription.ts
  • code/addons/docs/src/blocks/blocks/use-service-docgen.ts
  • code/addons/docs/src/blocks/blocks/use-service-story-docs.ts
  • code/core/src/shared/open-service/README.md
  • code/core/src/shared/open-service/query-runtime.ts
  • code/core/src/shared/open-service/query-state.ts
  • code/core/src/shared/open-service/service-runtime.test.ts
  • code/core/src/shared/open-service/service-runtime.ts
  • code/core/src/shared/open-service/types.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • code/core/src/shared/open-service/README.md
  • code/core/src/shared/open-service/query-state.ts
  • code/core/src/shared/open-service/service-runtime.test.ts

Comment thread code/addons/docs/src/blocks/blocks/use-query-subscription.ts Outdated
Comment thread code/addons/docs/src/blocks/blocks/use-service-story-docs.ts
Comment thread code/core/src/shared/open-service/query-runtime.ts
Comment thread code/core/src/shared/open-service/query-runtime.ts
JReinhold and others added 3 commits June 18, 2026 22:35
The extraction hot-refresh tracked extracted component ids in a separate Set
because the old bare query call fired the load behind the scenes. Now that
.get() is a pure read that never loads, read the component query directly so
the service state is the single source of truth.

Co-authored-by: Cursor <cursoragent@cursor.com>
A function selector cannot be folded into the string cacheKey, so track it by
reference as a real subscription dependency: changing the selector for the same
key now re-seeds and re-subscribes instead of returning a stale selected slice.

Co-authored-by: Cursor <cursoragent@cursor.com>
The old replacer substituted undefined with the literal '__undefined__', which
aliased an actual string input of that value (and optional object fields). Tag
undefined and objects with a discriminator so no two distinct inputs share a key.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:normal Run our default set of CI jobs (choose this for most PRs). maintenance User-facing maintenance tasks qa:skip Pull Requests that do not need any QA.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant