diff --git a/packages/core/media-store/src/factory.ts b/packages/core/media-store/src/factory.ts index 69c696d7..d19ea5f8 100644 --- a/packages/core/media-store/src/factory.ts +++ b/packages/core/media-store/src/factory.ts @@ -18,13 +18,13 @@ export type FacadeGetter = ( export type FacadeSetter = (value: T, stateOwners: StateOwners) => void; export type StateOwnerUpdateHandler = ( - handler: (value: T) => void, + handler: (value?: T) => void, stateOwners: StateOwners, -) => void; +) => (() => void) | void; export type ReadonlyFacadeProp = { get: FacadeGetter; - mediaEvents?: string[]; + stateOwnersUpdateHandlers?: StateOwnerUpdateHandler[]; }; export type FacadeProp = ReadonlyFacadeProp & { @@ -56,7 +56,7 @@ export function createMediaStore({ }) { const stateOwners: StateOwners = {}; const store = map({}); - const stateUpdateHandlers: Record void> = {}; + const stateUpdateHandlerCleanups: Record void)[]> = {}; const keys = Object.keys(stateMediator); function updateStateOwners(nextStateOwners: any) { @@ -64,34 +64,37 @@ export function createMediaStore({ return; } - let media = stateOwners.media; - if (media) { - for (const { mediaEvents = [] } of Object.values(stateMediator)) { - for (const mediaEvent of mediaEvents) { - media.removeEventListener( - mediaEvent, - stateUpdateHandlers[mediaEvent], - ); - delete stateUpdateHandlers[mediaEvent]; - } - } - } + // Clean up existing handlers + Object.entries(stateUpdateHandlerCleanups).forEach(([stateName, cleanups]) => { + cleanups.forEach(cleanup => cleanup?.()); + stateUpdateHandlerCleanups[stateName] = []; + }); Object.assign(stateOwners, nextStateOwners); - - media = stateOwners.media; store.set(getInitialState(stateMediator, stateOwners)); - if (media) { - for (const [stateName, stateObject] of Object.entries(stateMediator)) { - const { get, mediaEvents = [] } = stateObject; - for (const mediaEvent of mediaEvents) { - stateUpdateHandlers[mediaEvent] = () => - store.setKey(stateName, get(stateOwners)); - media.addEventListener(mediaEvent, stateUpdateHandlers[mediaEvent]); - } + // Set up new handlers + Object.entries(stateMediator).forEach(([stateName, stateObject]) => { + const { get, stateOwnersUpdateHandlers = [] } = stateObject; + + if (!stateUpdateHandlerCleanups[stateName]) { + stateUpdateHandlerCleanups[stateName] = []; } - } + + // Create handler that updates the store + const updateHandler = (value?: any) => { + const nextValue = value !== undefined ? value : get(stateOwners); + store.setKey(stateName, nextValue); + }; + + // Execute each stateOwnersUpdateHandler + stateOwnersUpdateHandlers.forEach(setupHandler => { + const cleanup = setupHandler(updateHandler, stateOwners); + if (typeof cleanup === 'function') { + stateUpdateHandlerCleanups[stateName]?.push(cleanup); + } + }); + }); } return { diff --git a/packages/core/media-store/src/state-mediators/audible.ts b/packages/core/media-store/src/state-mediators/audible.ts index 7e70736b..13b2620b 100644 --- a/packages/core/media-store/src/state-mediators/audible.ts +++ b/packages/core/media-store/src/state-mediators/audible.ts @@ -12,7 +12,17 @@ export const audible = { media.volume = 0.25; } }, - mediaEvents: ['volumechange'], + stateOwnersUpdateHandlers: [ + (handler: (value?: boolean) => void, stateOwners: any) => { + const { media } = stateOwners; + if (!media) return; + + const eventHandler = () => handler(); + media.addEventListener('volumechange', eventHandler); + + return () => media.removeEventListener('volumechange', eventHandler); + } + ], actions: { /** @TODO Refactor me to play more nicely with side effects that don't/can't correlate with set() API or aren't simple 1:1 with getter vs. setter (CJP) */ muterequest: () => true, @@ -34,7 +44,17 @@ export const audible = { media.mute = false; } }, - mediaEvents: ['volumechange'], + stateOwnersUpdateHandlers: [ + (handler: (value?: number) => void, stateOwners: any) => { + const { media } = stateOwners; + if (!media) return; + + const eventHandler = () => handler(); + media.addEventListener('volumechange', eventHandler); + + return () => media.removeEventListener('volumechange', eventHandler); + } + ], actions: { /** @TODO Refactor me to play more nicely with side effects that don't/can't correlate with set() API (CJP) */ volumerequest: ( @@ -52,6 +72,16 @@ export const audible = { if (media.volume < 0.75) return 'medium'; return 'high'; }, - mediaEvents: ['volumechange'], + stateOwnersUpdateHandlers: [ + (handler: (value?: 'high' | 'medium' | 'low' | 'off') => void, stateOwners: any) => { + const { media } = stateOwners; + if (!media) return; + + const eventHandler = () => handler(); + media.addEventListener('volumechange', eventHandler); + + return () => media.removeEventListener('volumechange', eventHandler); + } + ], }, }; diff --git a/packages/core/media-store/src/state-mediators/playable.ts b/packages/core/media-store/src/state-mediators/playable.ts index 32802b62..3aeed06e 100644 --- a/packages/core/media-store/src/state-mediators/playable.ts +++ b/packages/core/media-store/src/state-mediators/playable.ts @@ -8,7 +8,18 @@ export const playable = { const { media } = stateOwners; media?.[value ? 'pause' : 'play'](); }, - mediaEvents: ['play', 'playing', 'pause', 'emptied'], + stateOwnersUpdateHandlers: [ + (handler: (value?: boolean) => void, stateOwners: any) => { + const { media } = stateOwners; + if (!media) return; + + const eventHandler = () => handler(); + const events = ['play', 'playing', 'pause', 'emptied']; + events.forEach(event => media.addEventListener(event, eventHandler)); + + return () => events.forEach(event => media.removeEventListener(event, eventHandler)); + } + ], actions: { /** @TODO Refactor me to play more nicely with side effects that don't/can't correlate with set() API (CJP) */ playrequest: () => false, diff --git a/packages/core/media-store/src/state-mediators/temporal.ts b/packages/core/media-store/src/state-mediators/temporal.ts index fe603f9b..aea80b26 100644 --- a/packages/core/media-store/src/state-mediators/temporal.ts +++ b/packages/core/media-store/src/state-mediators/temporal.ts @@ -15,7 +15,18 @@ export const temporal = { if (!media || !isValidNumber(value)) return; media.currentTime = value; }, - mediaEvents: ['timeupdate', 'loadedmetadata'], + stateOwnersUpdateHandlers: [ + (handler: (value?: number) => void, stateOwners: any) => { + const { media } = stateOwners; + if (!media) return; + + const eventHandler = () => handler(); + const events = ['timeupdate', 'loadedmetadata']; + events.forEach(event => media.addEventListener(event, eventHandler)); + + return () => events.forEach(event => media.removeEventListener(event, eventHandler)); + } + ], actions: { /** @TODO Support more sophisticated seeking patterns like seek-to-live, relative seeking, etc. (CJP) */ seekrequest: ( @@ -39,7 +50,18 @@ export const temporal = { return media.duration; }, - mediaEvents: ['loadedmetadata', 'durationchange', 'emptied'], + stateOwnersUpdateHandlers: [ + (handler: (value?: number) => void, stateOwners: any) => { + const { media } = stateOwners; + if (!media) return; + + const eventHandler = () => handler(); + const events = ['loadedmetadata', 'durationchange', 'emptied']; + events.forEach(event => media.addEventListener(event, eventHandler)); + + return () => events.forEach(event => media.removeEventListener(event, eventHandler)); + } + ], }, seekable: { @@ -56,6 +78,17 @@ export const temporal = { return [Number(start.toFixed(3)), Number(end.toFixed(3))]; }, - mediaEvents: ['loadedmetadata', 'emptied', 'progress', 'seekablechange'], + stateOwnersUpdateHandlers: [ + (handler: (value?: [number, number] | undefined) => void, stateOwners: any) => { + const { media } = stateOwners; + if (!media) return; + + const eventHandler = () => handler(); + const events = ['loadedmetadata', 'emptied', 'progress', 'seekablechange']; + events.forEach(event => media.addEventListener(event, eventHandler)); + + return () => events.forEach(event => media.removeEventListener(event, eventHandler)); + } + ], }, };