mirror of
https://github.com/zoriya/v10.git
synced 2026-08-14 09:59:44 +00:00
docs(design): add feature slice design (#356)
This commit is contained in:
@@ -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<HTMLMediaElement>()({
|
||||
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` |
|
||||
Reference in New Issue
Block a user