Skip to content

refactor(store): v2 #362

Merged
mihar-22 merged 1 commit intomainfrom
refactor/store-v2-api
Jan 31, 2026
Merged

refactor(store): v2 #362
mihar-22 merged 1 commit intomainfrom
refactor/store-v2-api

Conversation

@mihar-22
Copy link
Copy Markdown
Member

@mihar-22 mihar-22 commented Jan 31, 2026

Ref: #320

Summary

Complete redesign of the store API to be simpler, more ergonomic, and Zustand-inspired. This reduces type complexity by ~60% while maintaining all functionality.

What Changed

Store Access: Flat instead of Nested

Before
// Nested access
const { paused, volume } = store.state;
await store.request.play();
store.request.setVolume(0.5);
After
// Flat access directly on store
const { paused, volume } = store;
await store.play();
store.setVolume(0.5);

Feature Definition: Factory Pattern

Before
const volumeFeature = createFeature<HTMLMediaElement>()({
  initialState: { volume: 1, muted: false },
  
  getSnapshot: ({ target }) => ({
    volume: target.volume,
    muted: target.muted,
  }),
  
  subscribe: ({ target, update, signal }) => {
    target.addEventListener('volumechange', update, { signal });
  },
  
  request: {
    setVolume(volume: number, { target }) {
      target.volume = volume;
    },
    toggleMute: {
      key: 'mute',
      handler(_, { target }) {
        target.muted = !target.muted;
        return target.muted;
      },
    },
  },
});
After
const volumeFeature = defineFeature<HTMLMediaElement>()({
  // State factory returns initial state AND actions together
  state: ({ task }) => ({
    volume: 1,
    muted: false,
    
    changeVolume(volume: number) {
      return task({
        key: 'volume',
        handler({ target }) {
          target.volume = Math.max(0, Math.min(1, volume));
          return target.volume;
        },
      });
    },
    
    toggleMute() {
      return task({
        key: 'mute',
        handler({ target }) {
          target.muted = !target.muted;
          return target.muted;
        },
      });
    },
  }),
  
  getSnapshot: ({ target }) => ({
    volume: target.volume,
    muted: target.muted,
  }),
  
  subscribe: ({ target, update, signal }) => {
    listen(target, 'volumechange', update, { signal });
  },
});

Request Metadata: Chained instead of Last Argument

Before
// Metadata as last argument (awkward for void inputs)
store.request.play(undefined, { source: 'user' });
store.request.seek(30, { source: 'keyboard' });
After
// Fluent chain - cleaner API
store.meta({ source: 'user' }).play();
store.meta(clickEvent).seek(30);

// Chain multiple actions with same meta
const m = store.meta(event);
m.play();
m.seek(30);

Task Tracking: Minimal instead of Full Lifecycle

Before
// Full task state machine
const { play: task } = store.queue.tasks;
if (isPendingTask(task)) { ... }
if (isSuccessTask(task)) { ... }
if (isErrorTask(task)) { ... }

// Subscribe to queue
store.queue.subscribe(() => { ... });
After
// Minimal pending tracking
if (store.pending.playback) {
  console.log('Task running since:', store.pending.playback.startedAt);
}

// Callbacks for observability
const store = createStore({
  features: [...],
  onTaskStart: ({ key, meta }) => console.log('Started:', key),
  onTaskEnd: ({ key, meta, error }) => console.log('Ended:', key),
});

Removed

Feature Reason
Queue public API Internal concern, not needed for consumers
Guards & combinators Just use code at start of handler
Computed values Derive in component or use selectors
SnapshotController StoreController provides flat access
useSnapshot useStore provides flat access
useQueue No public queue API
Request<Input, Output> Types inferred from factory return

Type Simplification

Metric Before After Change
Type definitions ~25 ~10 -60%
Lines of type code ~150 ~50 -65%
Inference depth 4+ levels 1-2 levels -50%

Platform Bindings

Lit

const { StoreMixin, ProviderMixin, ContainerMixin, StoreController } = createStore({
  features: [playbackFeature],
});

class MyControl extends LitElement {
  #store = new StoreController(this);
  
  render() {
    const { volume, setVolume } = this.#store.value;
    return html`<input type="range" .value=${volume} @input=${e => setVolume(e.target.value)} />`;
  }
}

React

const { Provider, useStore } = createStore({
  features: [playbackFeature],
});

function VolumeSlider() {
  const { volume, setVolume } = useStore();
  return <input type="range" value={volume} onChange={e => setVolume(e.target.value)} />;
}

Migration

No migration needed — no external users yet.

Files Changed

  • 64 files modified
  • ~1000 lines net reduction
  • All tests updated and passing (132 store + 34 core)

BREAKING CHANGE: Complete API redesign

- defineFeature() replaces createFeature()
- Flat store access (store.x, store.y()) replaces store.state/store.request
- task() helper replaces request config objects
- store.meta(event).action() replaces metadata as last argument
- Removed: Queue public API, Guards, Computed values
- Removed: SnapshotController, useSnapshot, useQueue
- Renamed: StoreAttachMixin → ContainerMixin, StoreProviderMixin → ProviderMixin
@vercel
Copy link
Copy Markdown

vercel bot commented Jan 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
vjs-10-website Ignored Ignored Jan 31, 2026 0:30am

Request Review

@mihar-22 mihar-22 changed the title refactor(store)!: v2 API with flat access and simplified types refactor(store): v2 Jan 31, 2026
@mihar-22 mihar-22 merged commit fe892cb into main Jan 31, 2026
4 checks passed
@mihar-22 mihar-22 deleted the refactor/store-v2-api branch January 31, 2026 12:34
@mihar-22 mihar-22 mentioned this pull request Feb 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant