Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Add UniversalStore API to sync state/events between multiple environments #30445

Merged
merged 36 commits into from
Feb 5, 2025

Conversation

JReinhold
Copy link
Contributor

@JReinhold JReinhold commented Feb 3, 2025

Works on #30201

What I did

Added initial implementation of the UniversalStore.

See this overview of tests and note the skipped ones are todo (potentially in a follow-up):

image

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

To test this out manually you can make the following changes to the UI Storybook:

  1. add './my-preset.ts to the list of addons in code/.storybook/main.ts
  2. add code/.storybook/my-preset.ts with:
import { UniversalStore } from 'storybook/internal/core-server';

type State = {
  count: number;
  otherCount: number;
  text: string;
};

type Event = {
  type: 'TRIGGER_COOL_STUFF';
  payload: {
    amount: number;
  };
};

const serverStore = UniversalStore.create<State, Event>({
  id: 'my-uni-store',
  leader: true,
  initialState: { count: 0, otherCount: 10, text: 'hello' },
});

serverStore.untilReady().then(() => {
  console.log('💪💪💪 PRESET IS READY');
});

serverStore.onStateChange((state) => {
  console.log('✅✅✅ STATE CHANGED IN PRESET', state);
});

serverStore.subscribe('TRIGGER_COOL_STUFF', (event) => {
  console.log('🎉🎉🎉 TRIGGER COOL STUFF!!!', event.payload.amount);
});
  1. modify code/.storybook/manager.tsx to be:
import React from 'react';

import { WithTooltip } from 'storybook/internal/components';
import { UniversalStore, addons, types, useUniversalStore } from 'storybook/internal/manager-api';

import { startCase } from 'es-toolkit/compat';

type State = {
  count: number;
  otherCount: number;
  text: string;
};

type Event = {
  type: 'TRIGGER_COOL_STUFF';
  payload: {
    amount: number;
  };
};

const managerStore =
  globalThis.CONFIG_TYPE === 'PRODUCTION'
    ? UniversalStore.create<State, Event>({
        id: 'my-uni-store',
        leader: true,
        initialState: {
          count: 0,
          otherCount: 0,
          text: 'manager is leader now 😈',
        },
      })
    : UniversalStore.create<State, Event>({
        id: 'my-uni-store',
      });

managerStore.onStateChange((state) => {
  console.log('✅✅✅ STATE CHANGED IN MANAGER', state);
});

managerStore.untilReady().then(() => {
  console.log('💪💪💪 MANAGER IS READY');
});

const Panel = () => {
  const [state, setState] = useUniversalStore(managerStore);
  const renderCountRef = React.useRef(0);
  React.useEffect(() => {
    renderCountRef.current++;
  });
  return (
    <div>
      <pre>
        <code>{JSON.stringify(state, null, 2)}</code>
      </pre>
      <button onClick={() => setState((s) => ({ ...s, count: s.count + 1 }))}>Increment</button>
      <input
        value={state?.text}
        onChange={(e) => setState((s) => ({ ...s, text: e.target.value }))}
      />
      <button
        onClick={() => managerStore.send({ type: 'TRIGGER_COOL_STUFF', payload: { amount: 20 } })}
      >
        Cool Trigger
      </button>
      <pre>
        <code>Renders: {renderCountRef.current}</code>
      </pre>
    </div>
  );
};

const Tool = () => {
  const [state, setState] = useUniversalStore(managerStore, (s) => s?.count);
  const renderCountRef = React.useRef(0);
  React.useEffect(() => {
    renderCountRef.current++;
  });

  return (
    <WithTooltip
      placement="top"
      tooltip={() => {
        return (
          <div>
            <pre>
              <code>{JSON.stringify(state, null, 2)}</code>
            </pre>
            <button onClick={() => setState((s) => ({ ...s, count: s.count + 1 }))}>
              Increment
            </button>
            <pre>
              <code>Renders: {renderCountRef.current}</code>
            </pre>
          </div>
        );
      }}
    >
      UniversalStore Tester
    </WithTooltip>
  );
};

addons.register('my-uni-addon', (api) => {
  console.log('LOG: registrering addon in manager');

  addons.add('my-uni-addon-panel', {
    title: 'UniversalStore Tester',
    type: types.PANEL,
    render: () => {
      return <Panel />;
    },
  });

  addons.add('my-uni-addon-toolbar', {
    title: 'UniversalStore Tester',
    type: types.TOOL,
    render: () => {
      return <Tool />;
    },
  });
});

addons.setConfig({
  sidebar: {
    renderLabel: ({ name, type }) => (type === 'story' ? name : startCase(name)),
  },
});
  1. add the following to code/.storybook/preview.tsx
import { UniversalStore, useUniversalStore } from 'storybook/internal/preview-api';

...

type State = {
  count: number;
  otherCount: number;
  text: string;
};

type Event = {
  type: 'TRIGGER_COOL_STUFF';
  payload: {
    amount: number;
  };
};

console.log('LOG: creating preview store');
const previewStore = UniversalStore.create<State, Event>({
  id: 'my-uni-store',
  debug: true,
});
previewStore.onStateChange((state) => {
  console.log('🥳🥳🥳 STATE CHANGED IN PREVIEW', state);
});

previewStore.untilReady().then(() => {
  console.log('💪💪💪 PREVIEW IS READY');
});

...

export const decorators = [
  (Story) => {
    const [state, setState] = useUniversalStore(previewStore, (s) => s?.text);
    const renderCountRef = React.useRef(0);
    React.useEffect(() => {
      renderCountRef.current++;
    });
    return (
      <div>
        <pre>
          <code>{JSON.stringify(state, null, 2)}</code>
        </pre>
        <button onClick={() => setState((s) => ({ ...s, count: s.count + 1 }))}>Increment</button>
        <input value={state} onChange={(e) => setState((s) => ({ ...s, text: e.target.value }))} />
        <button
          onClick={() => previewStore.send({ type: 'TRIGGER_COOL_STUFF', payload: { amount: 20 } })}
        >
          Cool Trigger
        </button>
        <pre>
          <code>Renders: {renderCountRef.current}</code>
        </pre>

        <Story />
      </div>
    );
  },
...

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

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

  • 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 canary-release-pr.yml --field pr=<PR_NUMBER>

name before after diff z %
createSize 0 B 0 B 0 B - -
generateSize 78.3 MB 78 MB -309 kB 0.34 -0.4%
initSize 131 MB 131 MB -145 kB 2.08 -0.1%
diffSize 53 MB 53.2 MB 165 kB 5.59 0.3%
buildSize 7.17 MB 7.22 MB 48.1 kB 136.41 0.7%
buildSbAddonsSize 1.85 MB 1.87 MB 25.5 kB 311.33 1.4%
buildSbCommonSize 195 kB 195 kB 0 B - 0%
buildSbManagerSize 1.86 MB 1.88 MB 14.1 kB 2561.77 0.8%
buildSbPreviewSize 0 B 0 B 0 B - -
buildStaticSize 0 B 0 B 0 B - -
buildPrebuildSize 3.91 MB 3.95 MB 39.6 kB 499.56 1%
buildPreviewSize 3.26 MB 3.27 MB 8.52 kB 28.12 0.3%
testBuildSize 0 B 0 B 0 B - -
testBuildSbAddonsSize 0 B 0 B 0 B - -
testBuildSbCommonSize 0 B 0 B 0 B - -
testBuildSbManagerSize 0 B 0 B 0 B - -
testBuildSbPreviewSize 0 B 0 B 0 B - -
testBuildStaticSize 0 B 0 B 0 B - -
testBuildPrebuildSize 0 B 0 B 0 B - -
testBuildPreviewSize 0 B 0 B 0 B - -
name before after diff z %
createTime 10s 24.5s 14.5s 0.87 59.2%
generateTime 21.5s 18.4s -3s -106ms -0.77 -16.9%
initTime 13.4s 11s -2s -419ms -1.11 -21.9%
buildTime 9s 8.4s -624ms -0.37 -7.4%
testBuildTime 0ms 0ms 0ms - -
devPreviewResponsive 5s 5.7s 742ms 0.14 12.8%
devManagerResponsive 3.7s 4.2s 532ms 0.15 12.4%
devManagerHeaderVisible 952ms 758ms -194ms -0.27 -25.6%
devManagerIndexVisible 956ms 835ms -121ms -0.08 -14.5%
devStoryVisibleUncached 4.3s 2.9s -1s -390ms -1.11 -46.5%
devStoryVisible 983ms 834ms -149ms -0.14 -17.9%
devAutodocsVisible 854ms 825ms -29ms 0.68 -3.5%
devMDXVisible 870ms 877ms 7ms 1.29 0.8%
buildManagerHeaderVisible 907ms 646ms -261ms -0.62 -40.4%
buildManagerIndexVisible 1s 739ms -294ms -0.65 -39.8%
buildStoryVisible 889ms 629ms -260ms -0.59 -41.3%
buildAutodocsVisible 743ms 561ms -182ms -0.33 -32.4%
buildMDXVisible 637ms 563ms -74ms -0.4 -13.1%

Greptile Summary

Introduces a new UniversalStore API for synchronizing state and events across different Storybook environments (server, manager UI, preview) with a leader-follower pattern.

  • Added code/core/src/shared/universal-store/index.ts implementing core UniversalStore functionality with state sync and event handling
  • Added code/core/src/shared/universal-store/use-universal-store-manager.ts and use-universal-store-preview.ts with environment-specific React hooks
  • Added comprehensive test coverage in code/core/src/shared/universal-store/index.test.ts for store creation, state management and event handling
  • Added type definitions in code/core/src/shared/universal-store/types.ts for type-safe state and event handling
  • Exported API as experimental features through manager-api, preview-api and core-server modules

Sorry, something went wrong.

Copy link

nx-cloud bot commented Feb 3, 2025

View your CI Pipeline Execution ↗ for commit 8557851.

Command Status Duration Result
nx run-many -t build --parallel=3 ✅ Succeeded 1m 47s View ↗

☁️ Nx Cloud last updated this comment at 2025-02-04 21:47:15 UTC

@JReinhold JReinhold marked this pull request as ready for review February 3, 2025 10:28
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

17 file(s) reviewed, 19 comment(s)
Edit PR Review Bot Settings | Greptile

@storybook-pr-benchmarking
Copy link

storybook-pr-benchmarking bot commented Feb 3, 2025

Package Benchmarks

Commit: 8557851, ran on 4 February 2025 at 21:55:02 UTC

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

@storybook/core

Before After Difference
Dependency count 54 54 0
Self size 19.05 MB 19.22 MB 🚨 +165 KB 🚨
Dependency size 14.44 MB 14.44 MB 0 B
Bundle Size Analyzer Link Link

storybook

Before After Difference
Dependency count 55 55 0
Self size 22 KB 22 KB 0 B
Dependency size 33.50 MB 33.66 MB 🚨 +165 KB 🚨
Bundle Size Analyzer Link Link

sb

Before After Difference
Dependency count 56 56 0
Self size 1 KB 1 KB 0 B
Dependency size 33.52 MB 33.68 MB 🚨 +165 KB 🚨
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 388 388 0
Self size 503 KB 503 KB 0 B
Dependency size 75.37 MB 75.54 MB 🚨 +165 KB 🚨
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 277 277 0
Self size 617 KB 617 KB 0 B
Dependency size 65.45 MB 65.62 MB 🚨 +165 KB 🚨
Bundle Size Analyzer Link Link

create-storybook

Before After Difference
Dependency count 113 113 0
Self size 1.11 MB 1.11 MB 0 B
Dependency size 42.63 MB 42.79 MB 🚨 +165 KB 🚨
Bundle Size Analyzer Link Link

JReinhold and others added 8 commits February 3, 2025 13:25
Co-authored-by: Valentin Palkovic <valentin@chromatic.com>

Verified

This commit was signed with the committer’s verified signature.
gsmet Guillaume Smet

Verified

This commit was signed with the committer’s verified signature.
gsmet Guillaume Smet
…ybook into feature/universal-store

Verified

This commit was signed with the committer’s verified signature.
gsmet Guillaume Smet
…iple leaders detected

Verified

This commit was signed with the committer’s verified signature.
gsmet Guillaume Smet
@JReinhold
Copy link
Contributor Author

@valentinpalkovic made some changes:

image
image

  1. if a second leader is created, the first leader will now log an error, and put all instances in an error state. Nothing will throw, as the only way to implement that was with timeouts, causing everything to slow down. I think the error log is the right trade off.
  2. pulled selectors out of getState and onStateChange, as they aren't really necessary there, they are only useful in the hooks that are reactive. so the hooks implements the selector directly now, which also made all the typings a lot simpler.
  3. LEADER_CREATED and FOLLOWER_CREATED events are now emitted
  4. Added tests for useUniversalStore manager hook. the preview hooks are pretty bad to test so I won't bother.

Verified

This commit was signed with the committer’s verified signature.
gsmet Guillaume Smet

Partially verified

This commit is signed with the committer’s verified signature.
gsmet’s contribution has been verified via GPG key.
We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
@JReinhold JReinhold merged commit 81268fe into next Feb 5, 2025
55 of 58 checks passed
@JReinhold JReinhold deleted the feature/universal-store branch February 5, 2025 08:51
@github-actions github-actions bot mentioned this pull request Feb 5, 2025
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants