From fcee531a4522e429354c72936aff2dd650bb7a8c Mon Sep 17 00:00:00 2001 From: beefchimi Date: Fri, 22 Dec 2023 13:19:20 -0500 Subject: [PATCH] :sparkles: [Stack] Add queue event --- docs/api.md | 4 ++++ src/Stack.ts | 17 +++++++++------ src/tests/Stack.test.ts | 47 +++++++++++++++++++++++++++++++++++++++++ src/types.ts | 1 + 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/docs/api.md b/docs/api.md index 1a383db..2056175 100644 --- a/docs/api.md +++ b/docs/api.md @@ -240,6 +240,10 @@ soundStack.activeEvents; // Possible `StackState` values are: `idle`, `loading`, `playing`. (event: 'state', listener: (current: StackState) => void) +// Event called whenever the `keys` property changes. This is useful +// to subscribe to changes in the internal “sound queue”. +(event: 'queue', listener: (newKeys: SoundId[], oldKeys: SoundId[]) => void) + // Event called whenever the `volume` property changes. (event: 'volume', listener: (level: number) => void) diff --git a/src/Stack.ts b/src/Stack.ts index 3d328a4..4042f93 100644 --- a/src/Stack.ts +++ b/src/Stack.ts @@ -1,7 +1,7 @@ import {EmittenCommon} from 'emitten'; import {getErrorMessage, fetchAudioBuffer, scratchBuffer} from './helpers'; -import {clamp, msToSec, secToMs} from './utilities'; +import {arrayShallowEquals, clamp, msToSec, secToMs} from './utilities'; import {tokens} from './tokens'; import type { @@ -201,18 +201,23 @@ export class Stack extends EmittenCommon { ({id}) => !outOfBoundsIds.includes(id), ); - outOfBounds.forEach((expiredSound) => { - expiredSound.stop(); - }); - + outOfBounds.forEach((expiredSound) => expiredSound.stop()); this.#setQueue(filteredQueue); return newSound; } #setQueue(value: Sound[]) { + const oldKeys = [...this._keys]; + const newKeys = value.map(({id}) => id); + const identicalKeys = arrayShallowEquals(oldKeys, newKeys); + this.#queue = value; - this._keys = value.map(({id}) => id); + this._keys = newKeys; + + if (!identicalKeys) { + this.emit('queue', newKeys, oldKeys); + } } #setState(value: StackState) { diff --git a/src/tests/Stack.test.ts b/src/tests/Stack.test.ts index f7e5d3e..4ba6ef3 100644 --- a/src/tests/Stack.test.ts +++ b/src/tests/Stack.test.ts @@ -210,6 +210,28 @@ describe('Stack component', () => { expect(spySound3Stop).toBeCalled(); }); + it('emits `queue` event for each stopped Sound', async () => { + const spyQueue: StackEventMap['queue'] = vi.fn((_new, _old) => {}); + + mockStack.on('queue', spyQueue); + expect(spyQueue).not.toBeCalled(); + + await mockStack.prepare('One'); + await mockStack.prepare('Two'); + await mockStack.prepare('Three'); + + expect(spyQueue).toBeCalledTimes(3); + expect(spyQueue).toHaveBeenLastCalledWith( + ['One', 'Two', 'Three'], + ['One', 'Two'], + ); + + mockStack.stop(); + + expect(spyQueue).toBeCalledTimes(6); + expect(spyQueue).toHaveBeenLastCalledWith([], ['Three']); + }); + it('returns instance', async () => { await mockStack.prepare('Foo'); const instance = mockStack.stop(); @@ -293,6 +315,31 @@ describe('Stack component', () => { await expect(sound).resolves.toBeInstanceOf(Sound); await expect(sound).resolves.toHaveProperty('id', mockSoundId); }); + + it('emits `queue` event with new and old `keys`', async () => { + const mockSoundId1 = 'Foo'; + const mockSoundId2 = 'Bar'; + const spyQueue: StackEventMap['queue'] = vi.fn((_new, _old) => {}); + + mockStack.on('queue', spyQueue); + expect(spyQueue).not.toBeCalled(); + expect(mockStack.keys).toHaveLength(0); + + await mockStack.prepare(mockSoundId1); + + expect(spyQueue).toBeCalledTimes(1); + expect(spyQueue).toBeCalledWith([mockSoundId1], []); + expect(mockStack.keys).toHaveLength(1); + + await mockStack.prepare(mockSoundId2); + + expect(spyQueue).toBeCalledTimes(2); + expect(spyQueue).toBeCalledWith( + [mockSoundId1, mockSoundId2], + [mockSoundId1], + ); + expect(mockStack.keys).toHaveLength(2); + }); }); describe('#load()', () => { diff --git a/src/types.ts b/src/types.ts index 2765807..8329955 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,6 +51,7 @@ export interface StackError { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type StackEventMap = { state: (current: StackState) => void; + queue: (newKeys: SoundId[], oldKeys: SoundId[]) => void; volume: (level: number) => void; mute: (muted: boolean) => void; error: (message: StackError) => void;