refactor(store): rename feature to slice (#373)

This commit is contained in:
rahim
2026-02-02 17:30:47 +11:00
committed by GitHub
parent d28dda114c
commit a1d0383d41
40 changed files with 862 additions and 741 deletions
+5
View File
@@ -0,0 +1,5 @@
import { defineSlice } from '@videojs/store';
import type { PlayerTarget } from './types';
export const definePlayerFeature = defineSlice<PlayerTarget>();
+1
View File
@@ -1,3 +1,4 @@
export * from './feature';
export * from './store/features';
export * from './store/selectors';
export * from './types';
@@ -1,11 +1,9 @@
import type { InferFeatureState } from '@videojs/store';
import { defineFeature } from '@videojs/store';
import type { InferSliceState } from '@videojs/store';
import { listen, serializeTimeRanges } from '@videojs/utils/dom';
import type { PlayerTarget } from '../../types';
import { definePlayerFeature } from '../../feature';
export const bufferFeature = defineFeature<PlayerTarget>()({
export const bufferFeature = definePlayerFeature({
state: () => ({
/** Buffered time ranges as [start, end] tuples. */
buffered: [] as [number, number][],
@@ -29,4 +27,4 @@ export const bufferFeature = defineFeature<PlayerTarget>()({
},
});
export type BufferState = InferFeatureState<typeof bufferFeature>;
export type BufferState = InferSliceState<typeof bufferFeature>;
@@ -1,11 +1,9 @@
import type { InferFeatureState } from '@videojs/store';
import { defineFeature } from '@videojs/store';
import type { InferSliceState } from '@videojs/store';
import { listen } from '@videojs/utils/dom';
import type { PlayerTarget } from '../../types';
import { definePlayerFeature } from '../../feature';
export const playbackFeature = defineFeature<PlayerTarget>()({
export const playbackFeature = definePlayerFeature({
state: ({ task }) => ({
/** Whether playback is paused. */
paused: true,
@@ -59,4 +57,4 @@ export const playbackFeature = defineFeature<PlayerTarget>()({
},
});
export type PlaybackState = InferFeatureState<typeof playbackFeature>;
export type PlaybackState = InferSliceState<typeof playbackFeature>;
@@ -1,11 +1,10 @@
import type { InferFeatureState } from '@videojs/store';
import { CANCEL_ALL, defineFeature } from '@videojs/store';
import type { InferSliceState } from '@videojs/store';
import { CANCEL_ALL } from '@videojs/store';
import { listen } from '@videojs/utils/dom';
import type { PlayerTarget } from '../../types';
import { definePlayerFeature } from '../../feature';
export const sourceFeature = defineFeature<PlayerTarget>()({
export const sourceFeature = definePlayerFeature({
state: ({ task }) => ({
/** Current media source URL (null if none). */
source: null as string | null,
@@ -44,4 +43,4 @@ export const sourceFeature = defineFeature<PlayerTarget>()({
},
});
export type SourceState = InferFeatureState<typeof sourceFeature>;
export type SourceState = InferSliceState<typeof sourceFeature>;
@@ -1,6 +1,7 @@
import { createStore } from '@videojs/store';
import { describe, expect, it } from 'vitest';
import type { PlayerTarget } from '../../../types';
import { bufferFeature } from '../buffer';
describe('bufferFeature', () => {
@@ -11,7 +12,7 @@ describe('bufferFeature', () => {
seekable: createTimeRanges([[0, 120]]),
});
const store = createStore({ features: [bufferFeature] });
const store = createStore<PlayerTarget>()(bufferFeature);
store.attach({ media: video, container: null });
expect(store.state.buffered).toEqual([[0, 60]]);
@@ -27,7 +28,7 @@ describe('bufferFeature', () => {
seekable: createTimeRanges([[0, 120]]),
});
const store = createStore({ features: [bufferFeature] });
const store = createStore<PlayerTarget>()(bufferFeature);
store.attach({ media: video, container: null });
expect(store.state.buffered).toEqual([
@@ -42,7 +43,7 @@ describe('bufferFeature', () => {
seekable: createTimeRanges([[0, 100]]),
});
const store = createStore({ features: [bufferFeature] });
const store = createStore<PlayerTarget>()(bufferFeature);
store.attach({ media: video, container: null });
// Update the mock video's buffered range
@@ -63,7 +64,7 @@ describe('bufferFeature', () => {
seekable: createTimeRanges([[0, 100]]),
});
const store = createStore({ features: [bufferFeature] });
const store = createStore<PlayerTarget>()(bufferFeature);
store.attach({ media: video, container: null });
// Update the mock video to have no buffered content
@@ -1,6 +1,7 @@
import { createStore } from '@videojs/store';
import { describe, expect, it, vi } from 'vitest';
import type { PlayerTarget } from '../../../types';
import { playbackFeature } from '../playback';
describe('playbackFeature', () => {
@@ -13,7 +14,7 @@ describe('playbackFeature', () => {
readyState: HTMLMediaElement.HAVE_ENOUGH_DATA,
});
const store = createStore({ features: [playbackFeature] });
const store = createStore<PlayerTarget>()(playbackFeature);
store.attach({ media: video, container: null });
expect(store.state.paused).toBe(false);
@@ -28,7 +29,7 @@ describe('playbackFeature', () => {
readyState: HTMLMediaElement.HAVE_CURRENT_DATA,
});
const store = createStore({ features: [playbackFeature] });
const store = createStore<PlayerTarget>()(playbackFeature);
store.attach({ media: video, container: null });
expect(store.state.waiting).toBe(true);
@@ -40,7 +41,7 @@ describe('playbackFeature', () => {
currentTime: 5,
});
const store = createStore({ features: [playbackFeature] });
const store = createStore<PlayerTarget>()(playbackFeature);
store.attach({ media: video, container: null });
expect(store.state.started).toBe(true);
@@ -52,7 +53,7 @@ describe('playbackFeature', () => {
currentTime: 0,
});
const store = createStore({ features: [playbackFeature] });
const store = createStore<PlayerTarget>()(playbackFeature);
store.attach({ media: video, container: null });
expect(store.state.started).toBe(true);
@@ -61,7 +62,7 @@ describe('playbackFeature', () => {
it('updates on play event', () => {
const video = createMockVideo({ paused: true });
const store = createStore({ features: [playbackFeature] });
const store = createStore<PlayerTarget>()(playbackFeature);
store.attach({ media: video, container: null });
expect(store.state.paused).toBe(true);
@@ -76,7 +77,7 @@ describe('playbackFeature', () => {
it('updates on pause event', () => {
const video = createMockVideo({ paused: false });
const store = createStore({ features: [playbackFeature] });
const store = createStore<PlayerTarget>()(playbackFeature);
store.attach({ media: video, container: null });
expect(store.state.paused).toBe(false);
@@ -91,7 +92,7 @@ describe('playbackFeature', () => {
it('updates on ended event', () => {
const video = createMockVideo({ ended: false });
const store = createStore({ features: [playbackFeature] });
const store = createStore<PlayerTarget>()(playbackFeature);
store.attach({ media: video, container: null });
expect(store.state.ended).toBe(false);
@@ -106,7 +107,7 @@ describe('playbackFeature', () => {
it('stops listening when store is destroyed', () => {
const video = createMockVideo({});
const store = createStore({ features: [playbackFeature] });
const store = createStore<PlayerTarget>()(playbackFeature);
store.attach({ media: video, container: null });
store.destroy();
@@ -125,7 +126,7 @@ describe('playbackFeature', () => {
const video = createMockVideo({});
video.play = vi.fn().mockResolvedValue(undefined);
const store = createStore({ features: [playbackFeature] });
const store = createStore<PlayerTarget>()(playbackFeature);
store.attach({ media: video, container: null });
await store.play();
@@ -137,7 +138,7 @@ describe('playbackFeature', () => {
const video = createMockVideo({});
video.pause = vi.fn();
const store = createStore({ features: [playbackFeature] });
const store = createStore<PlayerTarget>()(playbackFeature);
store.attach({ media: video, container: null });
store.pause();
@@ -1,6 +1,7 @@
import { createStore } from '@videojs/store';
import { describe, expect, it, vi } from 'vitest';
import type { PlayerTarget } from '../../../types';
import { sourceFeature } from '../source';
describe('sourceFeature', () => {
@@ -12,7 +13,7 @@ describe('sourceFeature', () => {
readyState: HTMLMediaElement.HAVE_ENOUGH_DATA,
});
const store = createStore({ features: [sourceFeature] });
const store = createStore<PlayerTarget>()(sourceFeature);
store.attach({ media: video, container: null });
expect(store.state.source).toBe('https://example.com/video.mp4');
@@ -25,7 +26,7 @@ describe('sourceFeature', () => {
Object.defineProperty(video, 'currentSrc', { value: '', writable: false });
Object.defineProperty(video, 'readyState', { value: HTMLMediaElement.HAVE_NOTHING, writable: false });
const store = createStore({ features: [sourceFeature] });
const store = createStore<PlayerTarget>()(sourceFeature);
store.attach({ media: video, container: null });
expect(store.state.source).toBe(null);
@@ -38,7 +39,7 @@ describe('sourceFeature', () => {
readyState: HTMLMediaElement.HAVE_NOTHING,
});
const store = createStore({ features: [sourceFeature] });
const store = createStore<PlayerTarget>()(sourceFeature);
store.attach({ media: video, container: null });
expect(store.state.canPlay).toBe(false);
@@ -59,7 +60,7 @@ describe('sourceFeature', () => {
currentSrc: 'https://example.com/video.mp4',
});
const store = createStore({ features: [sourceFeature] });
const store = createStore<PlayerTarget>()(sourceFeature);
store.attach({ media: video, container: null });
expect(store.state.source).toBe('https://example.com/video.mp4');
@@ -81,7 +82,7 @@ describe('sourceFeature', () => {
readyState: HTMLMediaElement.HAVE_ENOUGH_DATA,
});
const store = createStore({ features: [sourceFeature] });
const store = createStore<PlayerTarget>()(sourceFeature);
store.attach({ media: video, container: null });
expect(store.state.canPlay).toBe(true);
@@ -106,7 +107,7 @@ describe('sourceFeature', () => {
const video = createMockVideo({});
video.load = vi.fn();
const store = createStore({ features: [sourceFeature] });
const store = createStore<PlayerTarget>()(sourceFeature);
store.attach({ media: video, container: null });
const result = await store.loadSource('https://example.com/new.mp4');
@@ -1,6 +1,7 @@
import { createStore } from '@videojs/store';
import { describe, expect, it } from 'vitest';
import type { PlayerTarget } from '../../../types';
import { timeFeature } from '../time';
describe('timeFeature', () => {
@@ -11,7 +12,7 @@ describe('timeFeature', () => {
duration: 120,
});
const store = createStore({ features: [timeFeature] });
const store = createStore<PlayerTarget>()(timeFeature);
store.attach({ media: video, container: null });
expect(store.state.currentTime).toBe(30);
@@ -24,7 +25,7 @@ describe('timeFeature', () => {
duration: Number.NaN,
});
const store = createStore({ features: [timeFeature] });
const store = createStore<PlayerTarget>()(timeFeature);
store.attach({ media: video, container: null });
expect(store.state.duration).toBe(0);
@@ -33,7 +34,7 @@ describe('timeFeature', () => {
it('updates on timeupdate event', () => {
const video = createMockVideo({ currentTime: 0 });
const store = createStore({ features: [timeFeature] });
const store = createStore<PlayerTarget>()(timeFeature);
store.attach({ media: video, container: null });
expect(store.state.currentTime).toBe(0);
@@ -48,7 +49,7 @@ describe('timeFeature', () => {
it('updates on durationchange event', () => {
const video = createMockVideo({ duration: 0 });
const store = createStore({ features: [timeFeature] });
const store = createStore<PlayerTarget>()(timeFeature);
store.attach({ media: video, container: null });
expect(store.state.duration).toBe(0);
@@ -63,7 +64,7 @@ describe('timeFeature', () => {
it('updates on seeked event', () => {
const video = createMockVideo({ currentTime: 0 });
const store = createStore({ features: [timeFeature] });
const store = createStore<PlayerTarget>()(timeFeature);
store.attach({ media: video, container: null });
// Update mock currentTime
@@ -79,7 +80,7 @@ describe('timeFeature', () => {
duration: 120,
});
const store = createStore({ features: [timeFeature] });
const store = createStore<PlayerTarget>()(timeFeature);
store.attach({ media: video, container: null });
// Update mock to empty state
@@ -96,7 +97,7 @@ describe('timeFeature', () => {
describe('seek', () => {
it('sets currentTime on target and waits for seeked event', async () => {
const video = createMockVideo({});
const store = createStore({ features: [timeFeature] });
const store = createStore<PlayerTarget>()(timeFeature);
store.attach({ media: video, container: null });
const resultPromise = store.seek(45);
@@ -1,6 +1,7 @@
import { createStore } from '@videojs/store';
import { describe, expect, it } from 'vitest';
import type { PlayerTarget } from '../../../types';
import { volumeFeature } from '../volume';
describe('volumeFeature', () => {
@@ -11,7 +12,7 @@ describe('volumeFeature', () => {
muted: false,
});
const store = createStore({ features: [volumeFeature] });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
expect(store.state.volume).toBe(0.8);
@@ -20,7 +21,7 @@ describe('volumeFeature', () => {
it('sets volumeAvailability on attach', () => {
const video = createMockVideo({});
const store = createStore({ features: [volumeFeature] });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
// Should be 'available' or 'unsupported' based on browser capability
@@ -30,7 +31,7 @@ describe('volumeFeature', () => {
it('updates on volumechange event', () => {
const video = createMockVideo({ volume: 1, muted: false });
const store = createStore({ features: [volumeFeature] });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
expect(store.state.volume).toBe(1);
@@ -49,7 +50,7 @@ describe('volumeFeature', () => {
describe('changeVolume', () => {
it('sets volume on target', async () => {
const video = createMockVideo({});
const store = createStore({ features: [volumeFeature] });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
const result = await store.changeVolume(0.7);
@@ -60,7 +61,7 @@ describe('volumeFeature', () => {
it('clamps volume to min 0', async () => {
const video = createMockVideo({});
const store = createStore({ features: [volumeFeature] });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
await store.changeVolume(-0.5);
@@ -70,7 +71,7 @@ describe('volumeFeature', () => {
it('clamps volume to max 1', async () => {
const video = createMockVideo({});
const store = createStore({ features: [volumeFeature] });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
await store.changeVolume(1.5);
@@ -82,7 +83,7 @@ describe('volumeFeature', () => {
describe('toggleMute', () => {
it('toggles mute from false to true', async () => {
const video = createMockVideo({ muted: false });
const store = createStore({ features: [volumeFeature] });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
const result = await store.toggleMute();
@@ -93,7 +94,7 @@ describe('volumeFeature', () => {
it('toggles mute from true to false', async () => {
const video = createMockVideo({ muted: true });
const store = createStore({ features: [volumeFeature] });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
const result = await store.toggleMute();
+4 -6
View File
@@ -1,11 +1,9 @@
import type { InferFeatureState } from '@videojs/store';
import { defineFeature } from '@videojs/store';
import type { InferSliceState } from '@videojs/store';
import { listen, onEvent } from '@videojs/utils/dom';
import type { PlayerTarget } from '../../types';
import { definePlayerFeature } from '../../feature';
export const timeFeature = defineFeature<PlayerTarget>()({
export const timeFeature = definePlayerFeature({
state: ({ task }) => ({
/** Current playback position in seconds. */
currentTime: 0,
@@ -44,4 +42,4 @@ export const timeFeature = defineFeature<PlayerTarget>()({
},
});
export type TimeState = InferFeatureState<typeof timeFeature>;
export type TimeState = InferSliceState<typeof timeFeature>;
@@ -1,11 +1,10 @@
import type { InferFeatureState } from '@videojs/store';
import { defineFeature } from '@videojs/store';
import type { InferSliceState } from '@videojs/store';
import { listen } from '@videojs/utils/dom';
import type { FeatureAvailability, PlayerTarget } from '../../types';
import { definePlayerFeature } from '../../feature';
import type { FeatureAvailability } from '../../types';
export const volumeFeature = defineFeature<PlayerTarget>()({
export const volumeFeature = definePlayerFeature({
state: ({ task }) => ({
/** Volume level from 0 (silent) to 1 (max). */
volume: 1,
@@ -50,7 +49,7 @@ export const volumeFeature = defineFeature<PlayerTarget>()({
},
});
export type VolumeState = InferFeatureState<typeof volumeFeature>;
export type VolumeState = InferSliceState<typeof volumeFeature>;
/** Check if volume can be programmatically set (fails on iOS Safari). */
function canSetVolume(): FeatureAvailability {
+6 -6
View File
@@ -1,4 +1,4 @@
import { createFeatureSelector } from '@videojs/store';
import { createSelector } from '@videojs/store';
import { bufferFeature } from './features/buffer';
import { playbackFeature } from './features/playback';
@@ -6,8 +6,8 @@ import { sourceFeature } from './features/source';
import { timeFeature } from './features/time';
import { volumeFeature } from './features/volume';
export const selectBuffer = createFeatureSelector(bufferFeature);
export const selectPlayback = createFeatureSelector(playbackFeature);
export const selectSource = createFeatureSelector(sourceFeature);
export const selectTime = createFeatureSelector(timeFeature);
export const selectVolume = createFeatureSelector(volumeFeature);
export const selectBuffer = createSelector(bufferFeature);
export const selectPlayback = createSelector(playbackFeature);
export const selectSource = createSelector(sourceFeature);
export const selectTime = createSelector(timeFeature);
export const selectVolume = createSelector(volumeFeature);
+8
View File
@@ -1,3 +1,5 @@
import type { AnySlice, Slice, Store, UnionSliceState } from '@videojs/store';
export interface Media extends HTMLMediaElement {}
export interface MediaContainer extends HTMLElement {}
@@ -8,3 +10,9 @@ export interface PlayerTarget {
}
export type FeatureAvailability = 'available' | 'unavailable' | 'unsupported';
export type PlayerFeature<State> = Slice<PlayerTarget, State>;
export type AnyPlayerFeature = AnySlice<PlayerTarget>;
export type PlayerStore<Features extends AnyPlayerFeature[] = []> = Store<PlayerTarget, UnionSliceState<Features>>;