Skip to content

Open Service: Add sync between server, manager and preview#35017

Merged
JReinhold merged 72 commits into
nextfrom
norbert/service-clients
Jun 6, 2026
Merged

Open Service: Add sync between server, manager and preview#35017
JReinhold merged 72 commits into
nextfrom
norbert/service-clients

Conversation

@ndelangen

@ndelangen ndelangen commented Jun 1, 2026

Copy link
Copy Markdown
Member

Tracked by: #34824

What I did

This PR ships open-service client runtimes — multi-master state sync across the dev server, manager, and preview — plus the bootstrap fixes and authoring-model tightening needed to make it reliable in real Storybook.

Problem

Open-service works well on the server (registry, static builds, docgen extraction), but manager and preview had no way to run the same reactive query/command surface or stay in sync when state changes. After a full reload, we also hit a race where the preview iframe registered and sent welcome-request before the manager had installed channel listeners, so preview state could stay stale while the manager toolbar looked correct.

Solution (three layers)

1. Multi-master sync (core)

Each runtime (server, manager, preview) runs a full ServiceRuntime from the same ServiceDefinition. There is no RPC proxy and no single writer.

  • State converges via last-write-wins stamped (version, clientId) in the channel envelope (never inside user state)
  • Sync uses Storybook's existing channel with a services: prefix:
    • services:welcome-request / services:welcome-reply — bootstrap handshake
    • services:patches — post-mutation whole-state broadcast
  • Relay topology: dev server and manager are hubs (bridge multiple transports); previews are leaves
  • Server registration now auto-joins sync at registration (command wrapping + connectRuntimeToChannel) — no separate connect step
  • Shared transport lives in service-transport.ts; merge/order rules in service-sync.ts

New client surface:

  • registerServiceClient / manager & preview registerService barrels
  • useServiceQueryuseSyncExternalStore-backed, value-deduped
  • useServiceCommand — stable bound async function (no built-in pending/error state)
  • setServiceChannel / getServiceChannel — one channel install per entry point

First real consumer: core/docgen service (services/docgen/) wired through the services preset when features.experimentalDocgenServer is enabled.

2. Reload bootstrap ordering (regression fix)

Fixed: manager toolbar shows dark after reload, preview canvas stays light.

Decision Choice
Manager boot order Option A: run provider.handleAPIloadAddons synchronously in ManagerProvider constructor, before iframe src is set
PostMessage delivery Flush manager outbound buffer on first inbound postMessage from preview (HTML load ≠ channel ready)
Hub push on register Relay hubs with version > 0 emit one services:patches when joining
Welcome retries Removed — one request is enough once manager listeners exist first
Iframe src gate / PREVIEW_IFRAME_LOADED_EVENT Rejected — redundant with Option A + inbound flush

Demo fixes: setColor handler on shared definition.ts (multi-master — any peer that invokes a command needs the handler); ThemedSetRoot no longer fights document.body background.

3. Definition vs registration layer (authoring model)

Aligned the type system and merge logic with how clients actually register (registerServiceClient(definition) — no registration object):

Layer Queries Commands
Definition (defineService) handler, load, staticPath, optional staticInputs schemas + handler (when shared across runtimes)
Server registration staticInputs only (story index / registry context) handler only (server-only deps)

ServiceQueryRegistration no longer accepts handler or load. applyRegistration merges only staticInputs for queries. Docgen getDocgen.load moved to definition.ts; server registration keeps staticInputs + extractDocgen.handler.

Planned follow-up (not in this PR): fire load on subscribe and .loaded() only, not on every sync query() call.

Known gap (documented, not a regression)

Load-driven push: command wrapping broadcasts external command calls, but mutations made inside a query's load hook (via raw ctx.self.commands) are not broadcast to peers. Fixing this needs a local-mutation hook + microtask coalescing — scoped as deliberate follow-up.

Internal demo

  • Background-service demo under code/.storybook/background-service/ (toolbar + preview subscribe)
  • Playwright regression: open-service-internal project → open-service-background.spec.ts

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

Unit / type tests (code/core/src/shared/open-service/):

yarn test service-client.test.ts
yarn test service-registration-sync.test.ts
yarn test service-registration.test.ts
yarn test service-runtime.test.ts
yarn test server.test.ts
yarn test services/docgen/server.test.ts
yarn test server.test-d.ts index.test-d.ts
yarn test use-service-command.test.tsx

Full open-service suite (excludes use-service-query.test.tsx — OOMs in some environments, pre-existing):

cd code && NODE_OPTIONS=--max_old_space_size=4096 \
  yarn vitest run src/shared/open-service \
  --exclude "**/use-service-query.test.tsx"

E2E regression (reload sync — manager + preview background color after full page reload):

cd code && yarn nx compile core && yarn storybook:ui          # terminal 1
cd code && yarn playwright test --project=open-service-internal  # terminal 2

Or CI-style: CI=1 yarn playwright test --project=open-service-internal

Build:

yarn nx compile core
yarn nx run-many -t check

Manual testing

  1. cd code && yarn nx compile core && yarn storybook:ui
  2. Open internal Storybook UI (port 6006)
  3. Background service: use the open-service background toolbar → pick Dark → hard reload → confirm manager toolbar and preview canvas are both dark (#1B1C1D)
  4. Open a second manager tab → change background → confirm both tabs converge
  5. (Optional, docgen) enable features.experimentalDocgenServer and verify docgen extraction still works on the server path

Restart Storybook after code/core changes; .storybook changes are HMR-live.

Documentation

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

Updated: code/core/src/shared/open-service/README.md


Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label
  • Make sure this PR contains one of the labels below:
    • feature request

Reviewer guide

Start here:

  1. code/core/src/shared/open-service/README.md
  2. service-sync.ts + service-transport.ts — protocol invariants (LWW, relay, loop prevention)
  3. service-client.ts — client registration + welcome/patches listeners
  4. service-registration.ts — server auto-sync at registration
  5. code/core/src/manager-api/root.tsx — synchronous loadAddons (reload fix)
  6. code/core/src/channels/postmessage/index.ts — inbound flush
  7. code/.storybook/background-service/ — end-to-end demo
  8. code/e2e-tests/open-service-background.spec.ts — regression gate

Key design choices:

  • Multi-master, not server-as-proxy
  • Reuse Storybook channel, not a dedicated transport
  • useServiceCommand returns bare async fn, not react-query mutation shape
  • Query behavior on definition; server registration = staticInputs (+ command handlers for server-only work)
  • Multi-master commands invoked from manager/preview must have handlers on the definition

Explicitly not shipped / rejected:

  • Welcome-request retry timers
  • Iframe src gate until manager hub ready
  • PREVIEW_IFRAME_LOADED_EVENT
  • Query handler / load at server registration time
  • Load-driven state push to peers (follow-up)
  • Subscribe-only load triggering (follow-up)

Summary by CodeRabbit

  • New Features

    • Introduced open-service framework enabling multi-master state synchronization across manager, preview, and server runtimes.
    • Added background color service example with toolbar control for dynamic preview theming.
    • New React hooks: useServiceQuery and useServiceCommand for consuming service state and commands.
  • Documentation

    • Expanded open-service README with architecture, sync protocols, and multi-master conflict resolution patterns.

ndelangen added 2 commits June 1, 2026 16:57
- Added a new background color service with manager, preview, and server components.
- Integrated service registration in Storybook's manager and preview files.
- Established a communication channel for state synchronization between manager and preview.
- Created tests for service registration and subscription behavior.
- Added a new background color service with manager, preview, and server components.
- Integrated service registration in Storybook's manager and preview files.
- Established a communication channel for state synchronization between manager and preview.
- Created tests for service registration and subscription behavior.
@ndelangen ndelangen force-pushed the norbert/service-clients branch from 0309863 to d4dd5a6 Compare June 2, 2026 07:23
@ndelangen ndelangen marked this pull request as ready for review June 2, 2026 07:23
Copilot AI review requested due to automatic review settings June 2, 2026 07:23

Copilot AI 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.

Pull request overview

Introduces a multi-master open-service client architecture: browsers (manager + preview) and the server each run a full local ServiceRuntime and keep state in sync over Storybook's manager↔preview channel through a welcome-handshake + patch-broadcast protocol. Adds React hooks for consuming services and an example background-color service to demonstrate end-to-end sync.

Changes:

  • New client/channel modules (service-client.ts, service-channel.ts, service-server-channel.ts) plus client.ts/manager.ts/preview.ts entrypoints, with multi-master sync, loop prevention via clientId, and deepAssign-based state application.
  • New React hooks (useServiceQuery via useSyncExternalStore with isEqual bailout, useServiceCommand) with tests.
  • Wires server-registered services into the channel in experimental_serverChannel, and adds a background-service example registered in code/.storybook manager/preview/server presets.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
code/core/src/shared/open-service/service-channel.ts Channel interface, event/payload constants, global channel slot, generateClientId.
code/core/src/shared/open-service/service-client.ts Client registry, registerServiceClient, welcome/patch protocol, command wrapping.
code/core/src/shared/open-service/service-server-channel.ts Server-side opt-in channel participation.
code/core/src/shared/open-service/service-registration.ts Exposes getServiceRuntime for server channel wiring.
code/core/src/shared/open-service/use-service-query.ts React hook with useSyncExternalStore + isEqual snapshot stability.
code/core/src/shared/open-service/use-service-command.ts React hook returning a stable command reference.
code/core/src/shared/open-service/{client,manager,preview}.ts Entrypoint barrels for each environment.
code/core/src/shared/open-service/{service-client,use-service-query,use-service-command}.test.* Tests for client sync, query hook, and command hook.
code/core/src/shared/open-service/README.md Documents client architecture, channel protocol, and hooks.
code/core/src/core-server/presets/common-preset.ts Connects every registered service to the dev server channel.
code/.storybook/services-preset.ts Registers the example background service.
code/.storybook/{preview.tsx,manager.tsx} Side-effect imports for the example service.
code/.storybook/background-service/{definition,server,preview,manager}.* Example background-color service across all three runtimes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread code/core/src/core-server/presets/common-preset.ts Outdated
Comment thread code/core/src/core-server/presets/common-preset.ts Outdated
Comment thread code/core/src/shared/open-service/service-transport-leaf.test.ts Outdated
Comment thread code/core/src/shared/open-service/manager.ts Outdated
Comment thread code/core/src/shared/open-service/use-service-query.ts Outdated
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

This PR implements cross-peer service synchronization for Storybook's open-service framework. It adds channel-based multi-master state reconciliation with snapshot syncing, introduces public API entrypoints (client, manager, preview, server), implements React hooks for query and command access, wires manager bootstrap and channel installation, includes comprehensive protocol and transport tests, demonstrates the architecture with a background-color example service, and expands documentation on architecture and testing practices.

Changes

Open-Service Multi-Master Sync & Example

Layer / File(s) Summary
Service channel contract and protocol constants
code/core/src/shared/open-service/service-channel.ts
Exports ServiceChannel interface (on/off/emit), event constants (SERVICE_SYNC_START, SERVICE_PATCHES, command invoke/result/error), payload type definitions for sync-start handshake and patch broadcasts, and utilities (getServiceChannel(), generateClientId()) to access the shared Storybook channel.
Snapshot reconciliation and sync primitives
code/core/src/shared/open-service/service-sync.ts
Implements SyncStamp with last-write-wins ordering (version then clientId), deepReconcile for in-place structural merge with deletion propagation and prototype-pollution guards, parseStampedSnapshot/parseSyncStart for untrusted payload validation, validateSyncedState for schema-backed state acceptance, and createSnapshotReconciler for per-service adoption logic.
Service definition and typing updates
code/core/src/shared/open-service/service-definition.ts, code/core/src/shared/open-service/types.ts, code/core/src/shared/open-service/service-runtime.ts, code/core/src/shared/open-service/fixtures.ts, code/core/src/shared/open-service/server.test-d.ts, code/core/src/shared/open-service/service-registration.test.ts
Adds optional state schema field to service definitions for full-state validation; simplifies ServiceQueryRegistration to accept only TState; adds schema-backed schemaCounterServiceDef fixture; updates tests to place load/handler in definition, staticInputs at registration, and asserts compile-time errors for invalid registration overrides.
Command broadcast and channel transport
code/core/src/shared/open-service/service-transport.ts
Exports wrapCommandsForBroadcast to post-execute patch emission with stamp advancement, and connectRuntimeToChannel to wire sync-start bootstrap, handle incoming patches with version gating, relay re-emission when enabled, and return teardown function.
Service registration and realm-global registry
code/core/src/shared/open-service/service-registration.ts, code/core/src/shared/open-service/service-registry.ts
Introduces symbol-keyed globalThis registry map; registerService validates duplicate ids, creates snapshot reconcilers, wraps commands for broadcast, connects to channel with relay/leaf role, and persists runtime/metadata for discovery; exports listServices(), describeService(), getService() (typed overloads), unregisterService(), and clearRegistry().
Public API entrypoints and re-exports
code/core/src/shared/open-service/client.ts, code/core/src/shared/open-service/manager.ts, code/core/src/shared/open-service/preview.ts, code/core/src/shared/open-service/server.ts, code/core/src/manager-api/index.ts, code/core/src/preview-api/index.ts, code/core/src/manager-api/index.mock.ts, code/core/src/manager/globals/exports.ts, code/core/build-config.ts, code/core/package.json
Defines client, manager (relay=true), preview (relay=false), and server (relay=true) entrypoints; re-exports through manager-api and preview-api; publishes storybook/open-service subpath; adds browser build entry and globals exports.
React hooks for query and command access
code/core/src/shared/open-service/use-service-query.ts, code/core/src/shared/open-service/use-service-command.ts, code/core/src/shared/open-service/use-service-query.test.tsx, code/core/src/shared/open-service/use-service-command.test.tsx
useServiceQuery subscribes via useSyncExternalStore with deep equality and referential snapshot stability; useServiceCommand memoizes command function references; tests validate hook behavior, re-render suppression, and reference stability across renders.
Manager bootstrap, channel installation, and postmessage flush
code/core/src/manager-api/modules/provider.ts, code/core/src/manager-api/root.tsx, code/core/src/core-server/presets/common-preset.ts, code/core/src/channels/postmessage/index.ts
Moves provider.handleAPI(fullAPI) to ManagerProvider constructor before first render so listeners exist before preview sync-start; installs channel in common-preset only when transport is available; flushes buffered postMessage on manager when preview first connects.
Protocol and transport test coverage
code/core/src/shared/open-service/service-registration-sync.test.ts, code/core/src/shared/open-service/service-transport-leaf.test.ts
Tests channel listener registration/teardown, command broadcast with monotonic version and stable clientId, sync-start handshake, patch acceptance with version gating, stale patch dropping, malformed payload resilience, schema validation rejection, relay re-broadcasting, and multi-peer convergence.
Background-color service example and demo
code/.storybook/background-service/definition.ts, code/.storybook/background-service/manager.tsx, code/.storybook/background-service/preview.ts, code/.storybook/background-service/server.ts, code/.storybook/manager.tsx, code/.storybook/preview.tsx, code/.storybook/services-preset.ts, e2e-tests/open-service-background.spec.ts, code/playwright.config.ts
Defines shared background-color service with getColor query and setColor command; manager registers addon that renders color swatches using useServiceQuery/useServiceCommand; preview subscribes and updates document.body.style.background; server logs color changes; E2E test verifies selection persists across reload.
Documentation updates and docgen integration
code/core/src/shared/open-service/README.md, code/core/src/shared/open-service/services/docgen/definition.ts, code/core/src/shared/open-service/services/docgen/server.ts, code/core/src/shared/open-service/index.ts
Expands README with entrypoint strategy, file layout, multi-master client architecture, deep reconciliation and relay behavior, hook usage and implementation, and testing/design guidance; updates docgen service to declare load in definition and use server-side registerService wrapper.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • storybookjs/storybook#34960: Reverts open-service server registration and preset wiring that the main PR's background-color example depends on.
  • storybookjs/storybook#34954: Modifies shared docgen service paths (definition.ts/server.ts) where the main PR also updates load hook semantics and registration import sources.
  • storybookjs/storybook#34961: Changes core services preset plumbing that directly affects how the main PR's background-service registration runs in Storybook.

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: 9

🧹 Nitpick comments (1)
code/core/src/shared/open-service/use-service-query.test.tsx (1)

110-128: ⚡ Quick win

Assert render count here, not just reference identity.

This currently passes even if the hook rerenders on a deep-equal emission, because result.current can still end up pointing at firstRef after that extra render. Add a render counter so this test actually locks down the “no rerender” contract.

Proposed test tightening
   it('maintains referential stability when result is deeply equal', async () => {
     const service = registerServiceClient(mutableRecordLookupServiceDef);

     await service.commands.assignRecordField({ entryId: 'a', fieldKey: 'k', fieldValue: 'v' });

-    const { result } = renderHook(() =>
-      useServiceQuery(service, 'getRecordFields', { entryId: 'a' })
-    );
+    let renderCount = 0;
+    const { result } = renderHook(() => {
+      renderCount++;
+      return useServiceQuery(service, 'getRecordFields', { entryId: 'a' });
+    });

     const firstRef = result.current;
+    const countAfterMount = renderCount;

     // Assign the same value again — deeply equal, so no re-render.
     await service.commands.assignRecordField({ entryId: 'a', fieldKey: 'k', fieldValue: 'v' });

     // Wait a tick to let any spurious re-renders fire.
     await new Promise<void>((resolve) => setTimeout(resolve, 20));

     expect(result.current).toBe(firstRef);
+    expect(renderCount).toBe(countAfterMount);
   });
🤖 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/core/src/shared/open-service/use-service-query.test.tsx` around lines
110 - 128, The test currently only asserts referential stability by comparing
result.current to firstRef, which can falsely pass if an extra render occurs and
then returns the same reference; modify the test that uses renderHook and
useServiceQuery (with registerServiceClient and mutableRecordLookupServiceDef
and service.commands.assignRecordField) to also track the number of renders by
using the renderHook result's rerender counter or a local renderCount variable
updated inside the hook callback, then assert that renderCount did not increase
after assigning the same deep-equal value so the test enforces "no rerender"
rather than just identical reference identity.
🤖 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/.storybook/background-service/manager.tsx`:
- Around line 40-53: The swatch button in manager.tsx (the <button> using props
label, onClick, value, active) lacks an accessible name/state; add an explicit
accessible name and state by setting aria-label={label} (or aria-labelledby if
you prefer) and expose selection using aria-pressed={active} (or aria-checked
with role="radio" if these are part of a radio group), and also ensure
type="button" is set so it won’t submit forms unintentionally; keep the existing
title for mouse hover but rely on aria attributes for screen readers.

In `@code/.storybook/background-service/preview.ts`:
- Around line 29-33: The subscription in
backgroundService.queries.getColor.subscribe is directly mutating
document.body.style.background which conflicts with ThemedSetRoot's reset;
instead, have the subscriber set a dedicated CSS custom property (e.g.,
--story-background-color) on document.documentElement or a stable container
element, or call a setter on ThemedSetRoot/theming API so the background flows
through the same styling path; update ThemedSetRoot (or the theme provider) to
read that CSS variable or accept the service-driven value so preview rerenders
and theme updates won't overwrite the service-chosen color.

In `@code/.storybook/services-preset.ts`:
- Around line 7-12: The preset comment is out of sync with implementation:
registerBackgroundService() is invoked unconditionally inside the exported
services function, but the comment implies STORYBOOK_OPEN_SERVICE_DEBUG toggles
"all examples." Either update the comment to state that
STORYBOOK_OPEN_SERVICE_DEBUG only gates the debug service (not
registerBackgroundService), or change the implementation to guard
registerBackgroundService() with the environment check
(process.env.STORYBOOK_OPEN_SERVICE_DEBUG) so that registerBackgroundService()
only runs when that variable is truthy; refer to the exported services function
and the registerBackgroundService symbol when making the change.

In `@code/core/src/shared/open-service/manager.ts`:
- Around line 36-39: Remove the re-export that pulls preview-only dependencies
into the manager public entrypoint: delete the line exporting everything from
'./preview.ts' in manager.ts and instead re-export the intended public API from
a module that does not import storybook/preview-api (for example, re-export the
same symbols from './client.ts' or another safe module). Specifically, remove
"export * from './preview.ts'" and add re-exports that expose only the public
hooks (e.g., alongside the existing "export { useServiceCommand }" and "export {
useServiceQuery }" ensure any other intended exports come from './client.ts' or
a non-preview module), leaving preview.ts isolated so it no longer influences
manager.ts.

In `@code/core/src/shared/open-service/README.md`:
- Around line 382-392: The two fenced code blocks containing ASCII diagrams in
README.md are missing language identifiers and trigger markdownlint MD040;
update each opening ``` to include a language tag like ```text for both ASCII
diagram blocks (the block showing the Manager process / Preview process diagram
and the block showing "Peer A (manager)            Channel              Peer B
(preview)" flow) so the fenced blocks become ```text ... ``` to satisfy the
linter; apply the same change to the later matching pair of blocks as well (the
second occurrence referenced in the comment).

In `@code/core/src/shared/open-service/service-client.ts`:
- Around line 123-148: deepAssign currently leaves keys present in target but
missing from source, preventing deletions from propagating; update deepAssign so
it first removes any keys from target that are not present in source, then for
each key in source perform the existing logic (recurse for plain objects,
replace arrays/primitives), ensuring deletions in snapshots are applied; refer
to the deepAssign(target: Record<string, unknown>, source: Record<string,
unknown>) function and adjust its loop to delete absent keys from target before
assigning or recursing.
- Around line 272-277: The onWelcomeReply handler currently applies every
welcome reply and can regress state; modify onWelcomeReply (and/or surrounding
registration logic) to only apply the first valid welcome reply: check payload
as WelcomeReplyPayload and after the first successful
applyReceivedState(p.state) (only when p.serviceId === definition.id &&
p.clientId !== ownClientId) unregister or remove the listener or set a boolean
flag (e.g., welcomeApplied) to ignore subsequent replies so delayed/stale
replies are ignored.

In `@code/core/src/shared/open-service/service-server-channel.ts`:
- Around line 37-53: The deepAssign function must ignore prototype-pollution
keys to prevent merging dangerous properties; update deepAssign to skip any key
equal to "__proto__", "constructor", or "prototype" before recursing or
assigning to target (both in the branch that calls deepAssign(tv, sv) and in the
else assignment), keeping the rest of the recursive logic intact for other keys.
- Around line 85-103: The onWelcomeRequest and onPatches handlers currently cast
payload: unknown to WelcomeRequestPayload/PatchesPayload and immediately
dereference fields, which can throw on malformed input; update both handlers
(onWelcomeRequest, onPatches) to first validate that payload is a non-null
object and contains the expected keys with appropriate types (e.g., typeof
serviceId === 'string' and typeof clientId === 'string'; for PatchesPayload also
ensure state is an object) before reading fields, and bail out (or log) if
validation fails; keep the existing guards (serviceId/ownClientId) and
subsequent actions (emit SERVICE_WELCOME_REPLY, runtime.commandSelf.setState and
deepAssign) unchanged once payload is validated.

---

Nitpick comments:
In `@code/core/src/shared/open-service/use-service-query.test.tsx`:
- Around line 110-128: The test currently only asserts referential stability by
comparing result.current to firstRef, which can falsely pass if an extra render
occurs and then returns the same reference; modify the test that uses renderHook
and useServiceQuery (with registerServiceClient and
mutableRecordLookupServiceDef and service.commands.assignRecordField) to also
track the number of renders by using the renderHook result's rerender counter or
a local renderCount variable updated inside the hook callback, then assert that
renderCount did not increase after assigning the same deep-equal value so the
test enforces "no rerender" rather than just identical reference identity.
🪄 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: 607a082e-c2ec-4bd7-994c-9ce740d7e602

📥 Commits

Reviewing files that changed from the base of the PR and between 8715ba8 and d4dd5a6.

📒 Files selected for processing (21)
  • code/.storybook/background-service/definition.ts
  • code/.storybook/background-service/manager.tsx
  • code/.storybook/background-service/preview.ts
  • code/.storybook/background-service/server.ts
  • code/.storybook/manager.tsx
  • code/.storybook/preview.tsx
  • code/.storybook/services-preset.ts
  • code/core/src/core-server/presets/common-preset.ts
  • code/core/src/shared/open-service/README.md
  • code/core/src/shared/open-service/client.ts
  • code/core/src/shared/open-service/manager.ts
  • code/core/src/shared/open-service/preview.ts
  • code/core/src/shared/open-service/service-channel.ts
  • code/core/src/shared/open-service/service-client.test.ts
  • code/core/src/shared/open-service/service-client.ts
  • code/core/src/shared/open-service/service-registration.ts
  • code/core/src/shared/open-service/service-server-channel.ts
  • code/core/src/shared/open-service/use-service-command.test.tsx
  • code/core/src/shared/open-service/use-service-command.ts
  • code/core/src/shared/open-service/use-service-query.test.tsx
  • code/core/src/shared/open-service/use-service-query.ts

Comment thread code/.storybook/background-service/manager.tsx Outdated
Comment thread code/.storybook/background-service/preview.ts Outdated
Comment thread code/.storybook/services-preset.ts Outdated
Comment thread code/core/src/shared/open-service/manager.ts Outdated
Comment thread code/core/src/shared/open-service/README.md Outdated
Comment thread code/core/src/shared/open-service/service-client.ts Outdated
Comment thread code/core/src/shared/open-service/service-client.ts Outdated
Comment thread code/core/src/shared/open-service/service-server-channel.ts Outdated
Comment thread code/core/src/shared/open-service/service-server-channel.ts Outdated
ndelangen added 5 commits June 2, 2026 14:05
…zation

- Updated service registration to automatically join the cross-peer sync protocol without a separate connect step.
- Introduced a schema validation mechanism for state synchronization to ensure integrity across runtimes.
- Implemented last-write-wins logic for concurrent state updates, improving conflict resolution.
- Added comprehensive tests for service registration, state synchronization, and validation behavior.
- Removed deprecated service-server-channel integration, consolidating functionality into service-sync.
- Added a new project configuration for the open-service internal demo in Playwright.
- Updated Storybook's preview components to synchronize body background color with the open-service background demo.
- Introduced a new event for when the preview iframe finishes loading, improving state synchronization.
- Enhanced the Viewport component to conditionally render the iframe based on addon loading state.
- Added end-to-end tests for the open-service background demo to ensure consistent behavior after reloads.
- Removed the PREVIEW_IFRAME_LOADED_EVENT export from the postMessage channel as it is no longer utilized.
- Simplified the IFrame component by eliminating the event dispatch for iframe load completion.
- Updated the PostMessageTransport class to remove references to the now-removed event.
- Enhanced the Viewport component to streamline iframe rendering logic.
- Adjusted the provider module to clean up state management related to addon loading.
- Updated ManagerProvider to run addon register callbacks before the first render, ensuring manager-side listeners are ready for preview JS events.
- Refactored the provider module to remove unused API handling logic during initialization.
- Simplified open-service tests by consolidating welcome-request retry logic and improving clarity in test cases for welcome-reply handling.
- Adjusted end-to-end tests to validate the reload bootstrap path and ensure synchronization between manager and preview components.
- Updated README to clarify the merging of `staticInputs` and handler overrides in service registration.
- Improved type safety in `server.test-d.ts` by adding type expectations for input and context in service handlers.
- Adjusted service registration tests to ensure handlers and load hooks are correctly defined on service definitions.
- Enhanced type definitions in `types.ts` to reflect the correct structure for service query registrations.
- Updated docgen service definition to ensure the load hook is correctly associated with the query definition.
@ndelangen ndelangen self-assigned this Jun 2, 2026
…a81' of github.com:storybookjs/storybook into norbert/service-clients
@ndelangen ndelangen changed the title Implement open-service architecture runtime sync OpenService: Implement architecture runtime sync Jun 2, 2026
Comment thread code/.storybook/background-service/server.ts Outdated
Comment thread code/core/src/shared/open-service/service-channel.ts Outdated
Comment thread code/core/src/shared/open-service/service-server-channel.ts Outdated
Comment thread code/core/src/shared/open-service/service-server-channel.ts Outdated
Comment thread code/core/src/shared/open-service/service-client.ts Outdated
Comment thread code/core/src/shared/open-service/service-client.ts Outdated
Comment thread code/core/src/shared/open-service/service-client.ts Outdated
Comment thread code/core/src/shared/open-service/use-service-command.ts
Comment thread code/.storybook/background-service/preview.ts Outdated
Comment thread code/core/src/core-server/presets/common-preset.ts Outdated
Comment thread code/core/src/shared/open-service/client.ts Outdated
Comment thread code/.storybook/background-service/manager.tsx Outdated
ndelangen and others added 5 commits June 5, 2026 14:35
…t and WebsocketTransport

Updated both PostMessageTransport and WebsocketTransport classes to utilize a local CHANNEL_OPTIONS constant instead of directly referencing globalThis.CHANNEL_OPTIONS. This change improves code clarity and consistency in accessing channel options across the transport implementations.
Enhanced the README documentation for the remote command execution section, clarifying the roles of requester and responder, the command invocation process, and the event structure. This update aims to provide clearer guidance on how commands are executed across different runtimes, improving overall understanding for developers.
…cution

OpenService: Implement remote command execution
@JReinhold JReinhold added core qa:skip Pull Requests that do not need any QA. and removed qa:needed Pull Requests that will need manual QA prior to release. labels Jun 5, 2026
Resolve docgen service conflicts by adopting next's id-based API and
getDocgenForAllComponents while keeping load hooks on the service
definition per the restricted registration model on this branch.

Co-authored-by: Cursor <cursoragent@cursor.com>
@JReinhold JReinhold removed the ci:normal Run our default set of CI jobs (choose this for most PRs). label Jun 5, 2026
@JReinhold JReinhold marked this pull request as draft June 5, 2026 20:27
@JReinhold JReinhold self-assigned this Jun 5, 2026
@JReinhold JReinhold changed the title OpenService: Implement architecture runtime sync Open Service: Add sync between server, manager and preview Jun 5, 2026
JReinhold and others added 2 commits June 6, 2026 00:22
…types

- Consolidate channel wiring behind a single connectServiceToChannel entry
  point and capture one channel reference (removes a broadcast/listener
  channel-divergence hazard).
- Use nanoid for the identity-critical client/call id instead of Math.random.
- Drop spurious `as unknown as Channel` casts in the addon stores.
- Move @preact/signals-core and deepsignal to devDependencies so they are
  bundled into core's dist (fixes resolution in symlinked sandboxes).
- Fix useServiceQuery input typing by inferring TInput/TOutput as direct type
  parameters so the void-vs-input arg branch reads the concrete query type.

Co-authored-by: Cursor <cursoragent@cursor.com>
…n state to objects

- Replace hand-rolled channel-payload parsers with Valibot schemas in
  service-channel.ts; derive payload types from the schemas and narrow with
  v.safeParse in the transport (removes the duplicated parse/isRecord helpers).
- Constrain service state to a plain object at the defineService boundary via
  a ServiceState type (rejects primitives, null, and arrays), matching the
  deep-signal / deep-reconcile requirements. Add type tests and document it.

Co-authored-by: Cursor <cursoragent@cursor.com>
@JReinhold JReinhold added the ci:normal Run our default set of CI jobs (choose this for most PRs). label Jun 6, 2026
@JReinhold JReinhold marked this pull request as ready for review June 6, 2026 11:40
Co-authored-by: Cursor <cursoragent@cursor.com>
@JReinhold

Copy link
Copy Markdown
Contributor

Post-merge update after taking over this PR:

  • Tightened the open-service transport implementation: consolidated channel wiring into connectServiceToChannel, removed extra channel casts, fixed useServiceQuery inference for input-bearing queries, and moved signal packages to dev dependencies so they are bundled with core.
  • Replaced hand-written channel payload parsing with Valibot schemas, including explicit object-only state validation, and documented/enforced the ServiceState object/non-array constraint in types and README coverage.
  • Reworked the internal Storybook sync demo: removed the global background-color side effect, added paired local-command and remote-command sync stories under core/src/shared/open-service/sync-test, and expanded internal e2e coverage for manager/preview sync, reload persistence, and cross-tab relay.

Validation done locally for the latest demo work:

  • yarn --cwd code lint:js:cmd ... --fix on touched files
  • Storybook Vitest focused on the sync-test stories
  • Internal Playwright e2e for open-service-sync.spec.ts against a fresh dev server: 6 passed

JReinhold and others added 2 commits June 6, 2026 13:46
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
JReinhold and others added 2 commits June 6, 2026 20:36
The setup-file ordering / context.project.config change is unrelated test
infra and does not belong in the open-service PR. Restore the next version.

Co-authored-by: Cursor <cursoragent@cursor.com>
The save-from-controls Playwright test failed once in CI with stale preview
text, but passes consistently locally (including CI=true). Retrigger checks.

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

Copy link
Copy Markdown
Contributor

Re: the outdated review thread on configureVitest — that change was reverted in 062e88b because it was unrelated test infra (fixing context.project.config vs context.vitest.config). Open-service tests still pass without it. Happy to land that fix in a separate PR.

@JReinhold JReinhold merged commit 78e4393 into next Jun 6, 2026
15 of 21 checks passed
@JReinhold JReinhold deleted the norbert/service-clients branch June 6, 2026 19:10
@JReinhold JReinhold mentioned this pull request Jun 6, 2026
54 tasks
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). core feature request qa:skip Pull Requests that do not need any QA.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants