From 096723c82d15581d3d7e151e6c1f7d3fc274d8d8 Mon Sep 17 00:00:00 2001 From: rahim Date: Fri, 30 Jan 2026 17:11:52 +1100 Subject: [PATCH] docs(design): add feature slice design (#356) --- internal/design/feature-slice-design.md | 171 ++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 internal/design/feature-slice-design.md diff --git a/internal/design/feature-slice-design.md b/internal/design/feature-slice-design.md new file mode 100644 index 00000000..3ae449b1 --- /dev/null +++ b/internal/design/feature-slice-design.md @@ -0,0 +1,171 @@ +--- +status: draft +date: 2025-01-29 +--- + +# Feature Slice Design + +## Problem + +Primitives don't know which preset the user chose: + +```tsx +// Inside @videojs/react - shipped to users +export function PlayButton() { + const player = usePlayer(); // What type is this? + // User will use custom feature configuration + // We don't know if `playbackFeature` is included +} +``` + +They need: + +1. Loosely typed access to the player +2. A way to check if a feature exists +3. Type narrowing when the feature is present + +## Solution + +A `FeatureSlice` provides typed, scoped access to a feature's state and requests. Two API layers: + +1. **Primitive** — generic, explicit store/feature +2. **Player-specific** — typed by registry, uses context + +## Design + +### Feature Definition + +Features have a typed `name` (literal type preserved): + +```ts +const playbackFeature = createFeature()({ + name: 'playback', // Type: 'playback' (literal) + initialState: { paused: true }, + request: { play, pause }, +}); +``` + +### Store API + +```ts +// By reference — typed from feature +store.getFeature(playbackFeature); + +// By name — typed from store's features array +store.getFeature('playback'); +``` + +Returns `FeatureSlice | undefined`. Flat access to state and requests: + +```ts +const playback = store.getFeature(playbackFeature); + +if (!playback) return null; + +playback.paused; // State (flat) +playback.play(); // Request (flat) +playback.subscribe(cb); // Scoped subscription +``` + +### Primitive Layer (`@videojs/store`) + +Generic APIs that require explicit store/feature: + +```ts +// React — explicit store + feature +const playback = useFeature(store, playbackFeature); + +// Lit — explicit store + feature +#playback = new FeatureController(host, store, playbackFeature); +``` + +### Player Layer (`@videojs/core/dom`, `@videojs/react`, `@videojs/html`) + +Typed by `PlayerFeatureRegistry`, uses context: + +```ts +// Registry (manually maintained) +interface PlayerFeatureRegistry { + playback: typeof playbackFeature; + volume: typeof volumeFeature; + time: typeof timeFeature; +} + +// Utility +getPlayerFeature(store, 'playback'); // Typed + +// React — uses player context +usePlayerFeature('playback'); // Typed, store from context + +// Lit — uses player context +new PlayerFeatureController(host, 'playback'); // Typed +``` + +### Usage Examples + +**React primitive (custom feature):** + +```tsx +function CustomControl() { + const store = useStore(); + const custom = useFeature(store, myCustomFeature); + // ... +} +``` + +**React player-specific:** + +```tsx +function PlayButton() { + const playback = usePlayerFeature('playback'); + + if (!playback) return null; + + // ... +} +``` + +**Lit player-specific:** + +```ts +class PlayButton extends ReactiveElement { + #core = new PlayButtonCore(); + #playback = new PlayerFeatureController(this, 'playback'); + + protected override update(changed: PropertyValues) { + super.update(changed); + applyElementProps(this, this.#core.getProps(this.#playback.value)); + } +} +``` + +## FeatureSlice + +A thin lens over the store — flat access, scoped subscription: + +- State keys exposed directly (not `.state.paused`) +- Request keys exposed directly (not `.request.play()`) +- `subscribe()` for scoped updates (reserved name) + +## Rationale + +1. **Type safety** — Types flow from feature reference or registry +2. **No collisions** — Features keyed by symbol (unique) +3. **Layered API** — Primitive layer is generic, player layer is typed and convenient +4. **Familiar pattern** — Similar to Jotai atoms, Vue InjectionKey + +## Alternatives Considered + +See [`rfc/player-api/primitives.md`](../../rfc/player-api/primitives.md) for the proxy-level API. + +## Files + +| Package | File | Change | +|---------|------|--------| +| `store` | `src/core/feature-slice.ts` | New — `FeatureSlice` class | +| `store` | `src/core/store.ts` | Add `getFeature()` | +| `store` | `src/react/use-feature.ts` | New — `useFeature` hook | +| `store` | `src/lit/feature-controller.ts` | New — `FeatureController` | +| `core/dom` | `src/store/registry.ts` | New — `PlayerFeatureRegistry` | +| `react` | `src/use-player-feature.ts` | New — `usePlayerFeature` hook | +| `html` | `src/player-feature-controller.ts` | New — `PlayerFeatureController` |