From 9b36f3acbfd4d8b4ad44f012a9aad1793600f720 Mon Sep 17 00:00:00 2001 From: Rahim Date: Sun, 1 Feb 2026 18:25:53 +1100 Subject: [PATCH] docs(store): add feature API redesign plan Merge getSnapshot and subscribe into single attach method with set(). --- .claude/plans/store/feature-api-redesign.md | 265 ++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 .claude/plans/store/feature-api-redesign.md diff --git a/.claude/plans/store/feature-api-redesign.md b/.claude/plans/store/feature-api-redesign.md new file mode 100644 index 00000000..501694cb --- /dev/null +++ b/.claude/plans/store/feature-api-redesign.md @@ -0,0 +1,265 @@ +# Feature API Redesign + +Merge `getSnapshot` and `subscribe` into a single `attach` method with explicit `set()`. + +## Overview + +| Before | After | +|--------|-------| +| `state` (factory) | `state` (factory) — unchanged | +| `getSnapshot` | Merged into `attach` | +| `subscribe` | Renamed to `attach`, `update` → `set` | + +## Current vs New + +### Before + +```ts +const playbackFeature = defineFeature()({ + state: ({ task }) => ({ + paused: true, + play: () => task({ handler: ({ target }) => target.play() }), + }), + + getSnapshot: ({ target }) => ({ + paused: target.paused, + }), + + subscribe: ({ target, update, signal }) => { + listen(target, 'play', update, { signal }); + listen(target, 'pause', update, { signal }); + }, +}); +``` + +### After + +```ts +const playbackFeature = defineFeature()({ + state: ({ task, target }) => ({ + paused: true, + play: () => task({ handler: () => target().play() }), + }), + + attach({ target, signal, set }) { + const sync = () => set({ paused: target.paused }); + + sync(); + + listen(target, 'play', sync, { signal }); + listen(target, 'pause', sync, { signal }); + }, +}); +``` + +## Changes + +### 1. Remove `getSnapshot` + +Initial sync now happens in `attach` by calling `set()` immediately. + +### 2. Rename `subscribe` → `attach` + +Better name — it's called when the store attaches to a target. + +### 3. Replace `update` → `set(partial)` + +Instead of calling `update()` which triggers `getSnapshot`, features call `set()` directly with the changed values: + +```ts +// Before: update() triggers getSnapshot() internally +subscribe: ({ target, update, signal }) => { + listen(target, 'play', update, { signal }); +} + +// After: set() directly with partial state +attach({ target, signal, set }) { + const sync = () => set({ paused: target.paused }); + + sync(); + + listen(target, 'play', sync, { signal }); +} +``` + +### 4. Add `get` to attach context + +For features that need to read current state during sync: + +```ts +attach({ target, signal, set, get }) { + const sync = () => { + const current = get(); + set({ started: current.started || !target.paused }); + }; + + sync(); + + listen(target, 'play', sync, { signal }); +} +``` + +## Type Changes + +```ts +// Remove +interface GetSnapshotContext { ... } +type GetSnapshot = ... + +// Rename + modify +interface SubscribeContext { ... } // Remove +interface AttachContext { // New + target: Target; + signal: AbortSignal; + set: (partial: Partial) => void; + get: () => Readonly; +} + +// Update FeatureConfig +interface FeatureConfig { + state: (ctx: StateFactoryContext) => State; + attach?: (ctx: AttachContext) => void; // Optional, replaces getSnapshot + subscribe +} +``` + +## Store Changes + +### Remove from `createStore` + +- `syncAll()` — features call `set()` in attach +- `syncFeature()` — no more `getSnapshot` to call +- `getSnapshot` handling — merged into attach + +### Simplified attach + +```ts +function attach(newTarget: Target): () => void { + // ... setup ... + + // Define context once, share across all features + const attachCtx = { + target: newTarget, + signal, + set: (partial: Partial) => state.patch(partial), + get: () => state.current, + }; + + for (const feature of features) { + feature.attach?.(attachCtx); + } + + return detach; +} +``` + +## Migration Examples + +### volume.ts + +```ts +// Before +export const volumeFeature = defineFeature()({ + state: ({ task }) => ({ + volume: 1, + muted: false, + changeVolume(volume: number) { + return task({ key: 'volume', handler: ({ target }) => { target.volume = volume; } }); + }, + }), + getSnapshot: ({ target }) => ({ volume: target.volume, muted: target.muted }), + subscribe: ({ target, update, signal }) => { + listen(target, 'volumechange', update, { signal }); + }, +}); + +// After +export const volumeFeature = defineFeature()({ + state: ({ task, target }) => ({ + volume: 1, + muted: false, + changeVolume(volume: number) { + return task({ key: 'volume', handler: () => { target().volume = volume; } }); + }, + }), + + attach({ target, signal, set }) { + const sync = () => set({ volume: target.volume, muted: target.muted }); + + sync(); + + listen(target, 'volumechange', sync, { signal }); + }, +}); +``` + +### buffer.ts (no actions, just sync) + +```ts +// Before +export const bufferFeature = defineFeature()({ + state: () => ({ + buffered: [] as [number, number][], + seekable: [] as [number, number][], + }), + getSnapshot: ({ target }) => ({ + buffered: serializeTimeRanges(target.buffered), + seekable: serializeTimeRanges(target.seekable), + }), + subscribe: ({ target, update, signal }) => { + listen(target, 'progress', update, { signal }); + listen(target, 'emptied', update, { signal }); + }, +}); + +// After +export const bufferFeature = defineFeature()({ + state: () => ({ + buffered: [] as [number, number][], + seekable: [] as [number, number][], + }), + + attach({ target, signal, set }) { + const sync = () => set({ + buffered: serializeTimeRanges(target.buffered), + seekable: serializeTimeRanges(target.seekable), + }); + + sync(); + + listen(target, 'progress', sync, { signal }); + listen(target, 'emptied', sync, { signal }); + }, +}); +``` + +## Files to Update + +### Store Package + +``` +packages/store/src/core/feature.ts + - Remove GetSnapshot, GetSnapshotContext, Subscribe, SubscribeContext + - Add AttachContext + - Update FeatureConfig (attach replaces getSnapshot + subscribe) + +packages/store/src/core/store.ts + - Remove syncAll(), syncFeature() + - Update attach() to call feature.attach with new context +``` + +### Core Package + +``` +packages/core/src/dom/store/features/playback.ts +packages/core/src/dom/store/features/volume.ts +packages/core/src/dom/store/features/time.ts +packages/core/src/dom/store/features/source.ts +packages/core/src/dom/store/features/buffer.ts +``` + +### Tests + +``` +packages/store/src/core/tests/feature.test.ts +packages/store/src/core/tests/store.test.ts +```