Created
January 19, 2020 16:45
-
-
Save Platekun/5c37a9796547862c7299ad6d4bbff5a9 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const WEB_PLAYBACK_API = 'https://api.spotify.com/v1/me/player'; | |
const InitialState = '@Initial'; | |
const ReadyState = '@Ready'; | |
const NotReadyState = '@NotReadyState'; | |
const IdleState = '@Idle'; | |
const PlayState = '@Play'; | |
const RequestingToPlayTrackState = '@Play/Track/Request'; | |
const RequestingToPlayAlbumState = '@Play/Album/Request'; | |
const RequestingToResumeTrackState = '@Resume/Request'; | |
const PlayingState = '@Play/Success'; | |
const FailedToPlayState = '@Play/Error'; | |
const PauseState = '@Pause'; | |
const RequestingToPauseTrackState = '@Pause/Request'; | |
const PausedState = '@Pause/Success'; | |
const ResettingCurrentTrackProgressState = '@Pause/ResettingCurrentTrackProgressState'; | |
const StoppingState = '@Stop'; | |
const RequestingToSkipPlaybackToPreviousTrackState = '@RequestingToSkipPlaybackToPreviousTrack'; | |
const RequestingToSkipPlaybackToNextTrackState = '@RequestingToSkipPlaybackToNextTrack'; | |
const InitializationErrorState = '@InitializationError'; | |
const AccountErrorState = '@AccountError'; | |
const AuthenticationErrorState = '@AuthenticationError'; | |
const PlaybackErrorState = '@PlaybackError'; | |
const TrackSelectedEvent = 'TRACK_SELECTED'; | |
const AlbumSelectedEvent = 'ALBUM_SELECTED'; | |
const TrackPausedEvent = 'TRACK_PAUSED'; | |
const FailedToPauseEvent = 'FAILED_TO_PAUSE_TRACK'; | |
const TrackResumedEvent = 'TRACK_RESUMED'; | |
const TrackStoppedEvent = 'TRACK_STOPPED'; | |
const FailedToStopEvent = 'FAILED_TO_STOP_TRACK'; | |
const NextTrackRequestedEvent = 'NEXT_TRACK_REQUESTED'; | |
const PreviousTrackRequestedEvent = 'PREVIOUS_TRACK_REQUESTED'; | |
const SkippedPlaybackToPreviousTrackEvent = 'SKIPPED_PLAYBACK_TO_PREVIOUS_TRACK_EVENT'; | |
const InitializationErrorOcurredEvent = 'INITIALIZATION_ERROR_OCURRED'; | |
const PlayerReadyEvent = 'PLAYER_READY'; | |
const PlayerNotReadyEvent = 'PLAYER_NOT_READY'; | |
const AuthenticationErrorOcurredEvent = 'AUTHENTICATION_ERROR_OCURRED'; | |
const AccountErrorOcurredEvent = 'ACCOUNT_ERROR_OCURRED'; | |
const PlaybackErrorOcurredEvent = 'PLAYBACK_ERROR_OCURRED'; | |
const PlayerStateChangedEvent = 'PLAYER_STATE_CHANGED'; | |
const setTrackSelectionAction = 'setTrackSelection'; | |
const setAlbumSelectionAction = 'setAlbumSelection'; | |
const updateRetryCountAction = 'updateRetryCount'; | |
const resetRetryCountAction = 'resetRetryCount'; | |
const logPlayerReadyAction = 'logPlayerReady'; | |
const saveDeviceIdAction = 'saveDeviceId'; | |
const savePlayerInstanceAction = 'savePlayerInstance'; | |
const logPlaybackErrorAction = 'logPlaybackError'; | |
const logAccountErrorAction = 'logAccountError'; | |
const logAuthenticationErrorAction = 'logAuthenticationError'; | |
const logInitializationErrorAction = 'logInitializationError'; | |
const logPlayerStateChangeAction = 'logPlayerStateChange'; | |
const setPlaybackStateAction = 'setPlaybackState'; | |
const logPlayerNotReadyAction = 'logPlayerNotReady'; | |
const setPlayerWasBeingPlayingBeforeStoppingFlagAction = 'setPlayerWasBeingPlayingBeforeStoppingFlag'; | |
const deletePlayerWasBeingPlayingBeforeStoppingFlagAction = 'deletePlayerWasBeingPlayingBeforeStoppingFlag'; | |
const setPlayerWasBeingPausedBeforeStoppingFlagAction = 'setPlayerWasBeingPausedBeforeStoppingFlag'; | |
const deletePlayerWasBeingPausedBeforeStoppingFlagAction = 'deletePlayerWasBeingPausedBeforeStoppingFlag'; | |
const playTrackService = 'playTrack'; | |
const playAlbumService = 'playAlbumService'; | |
const resumeTrackService = 'resumeTrack'; | |
const pauseTrackService = 'pauseTrack'; | |
const skipPlaybackToPreviousTrackService = 'skipPlaybackToPreviousTrack'; | |
const skipPlaybackToNextTrackService = 'skipPlaybackToNextTrack'; | |
const resetCurrentTrackProgressService = 'resetCurrentTrackPRogress'; | |
const stopTrackService = 'stopTrack'; | |
const keepRetryingGuard = 'keepRetrying'; | |
const isNotTheSameTrackThatIsPlaying = 'isAValidTrackToPlay'; | |
const wasPlayingBeforeStoppingGuard = 'wasPlayingBeforeStopping'; | |
const wasPausedBeforeStoppingGuard = 'wasPausedBeforeStopping'; | |
const lessThanTwoSecondsHasPassedGuard = 'lessThanTwoSecondsHasPassed'; | |
const moreThanTwoSecondsHasPassedGuard = 'moreThanTwoSecondsHasPassed'; | |
const playbackIsPausedGuard = 'playbackIsPaused'; | |
const playbackIsNotPausedGuard = 'playbackIsNotPaused'; | |
const getPlaybackStateCallback = 'getPlaybackState'; | |
async function prettifyError(res) { | |
let parsedResponse = await res.json(); | |
return JSON.stringify(parsedResponse, null, 2); | |
} | |
function checkOperationParam(opts) { | |
let { param, message, loggerService, boom = true } = opts; | |
if (param === null || typeof param === 'undefined' || param === '' || param.trim() === '') { | |
loggerService.log(message, 'error'); | |
if (boom) { | |
throw new Error(message); | |
} | |
} | |
} | |
function canSkipToPreviousTrack(ctx) { | |
let { playbackState } = ctx; | |
if (playbackState === null) { | |
return false; | |
} | |
let { disallows: { skipping_prev: skippingPrev }, track_window: { previous_tracks: previousTracks } } = playbackState; | |
return !skippingPrev || previousTracks.length !== 0; | |
} | |
async function pauseTrackServiceFn(ctx) { | |
let { deviceId, trackSelection, authService, loggerService } = ctx; | |
let token = authService.token; | |
let { spotifyUri } = trackSelection; | |
checkOperationParam({ | |
param: deviceId, | |
message: 'You need a device id in order to pause a track', | |
loggerService | |
}); | |
checkOperationParam({ | |
param: spotifyUri, | |
message: 'You need a spotify URI in order to pause a track', | |
loggerService | |
}); | |
checkOperationParam({ param: token, message: 'You need a JWT in order to pause a track', loggerService }); | |
let response = await fetch(`${WEB_PLAYBACK_API}/pause?device_id=${deviceId}`, { | |
method: 'PUT', | |
body: JSON.stringify({ uris: [spotifyUri] }), | |
headers: { | |
'Content-Type': 'application/json', | |
Authorization: `Bearer ${token}` | |
} | |
}); | |
if (!response.ok) { | |
let reason = await prettifyError(response); | |
let errorMessage = `Current track could not be paused: ${reason}`; | |
loggerService.log(errorMessage, 'error'); | |
throw new Error(errorMessage); | |
} | |
loggerService.log(`Current track was paused`, 'success'); | |
} | |
async function resetCurrentTrackProgressServiceFn(ctx) { | |
let { deviceId, authService, loggerService } = ctx; | |
let token = authService.token; | |
checkOperationParam({ | |
param: deviceId, | |
message: 'You need a device id in order to reset the current track progress', | |
loggerService | |
}); | |
checkOperationParam({ | |
param: token, | |
message: 'You need a JWT in order to reset the current track track progress', | |
loggerService | |
}); | |
let response = await fetch(`${WEB_PLAYBACK_API}/seek?device_id=${deviceId}&position_ms=${0}`, { | |
method: 'PUT', | |
headers: { | |
'Content-Type': 'application/json', | |
Authorization: `Bearer ${authService.token}` | |
} | |
}); | |
if (!response.ok) { | |
let reason = await prettifyError(response); | |
let errorMessage = `Failed to reset the current track progress: ${reason}`; | |
loggerService.log(errorMessage, 'error'); | |
throw new Error(errorMessage); | |
} | |
loggerService.log(`Current track progress was reset`, 'success'); | |
} | |
const playbackMachine = Machine({ | |
id: 'Playback Machine', | |
initial: InitialState, | |
states: { | |
[InitialState]: { | |
on: { | |
[PlayerReadyEvent]: { | |
target: ReadyState, | |
actions: [logPlayerReadyAction, saveDeviceIdAction, savePlayerInstanceAction] | |
}, | |
[PlayerNotReadyEvent]: { | |
target: NotReadyState, | |
actions: [logPlayerNotReadyAction] | |
} | |
} | |
}, | |
[ReadyState]: { | |
initial: IdleState, | |
states: { | |
[IdleState]: { | |
on: { | |
[TrackSelectedEvent]: { | |
target: PlayState, | |
actions: [setTrackSelectionAction], | |
// ? TODO: Do I need this? | |
cond: isNotTheSameTrackThatIsPlaying | |
}, | |
[AlbumSelectedEvent]: { | |
target: `${PlayState}.${RequestingToPlayAlbumState}`, | |
actions: [setAlbumSelectionAction] | |
} | |
} | |
}, | |
[PlayState]: { | |
initial: RequestingToPlayTrackState, | |
states: { | |
[RequestingToPlayTrackState]: { | |
invoke: { | |
id: 'invokePlayTrack', | |
src: playTrackService, | |
onDone: { | |
target: PlayingState, | |
actions: [resetRetryCountAction] | |
}, | |
onError: [ | |
{ | |
target: RequestingToPlayTrackState, | |
actions: [updateRetryCountAction], | |
cond: keepRetryingGuard | |
}, | |
{ | |
target: FailedToPlayState, | |
actions: [resetRetryCountAction] | |
} | |
] | |
} | |
}, | |
[RequestingToPlayAlbumState]: { | |
invoke: { | |
id: 'invokePlayAlbum', | |
src: playAlbumService, | |
onDone: { | |
target: PlayingState, | |
actions: [resetRetryCountAction] | |
}, | |
onError: [ | |
{ | |
target: RequestingToPlayAlbumState, | |
actions: [updateRetryCountAction], | |
cond: keepRetryingGuard | |
}, | |
{ | |
target: FailedToPlayState, | |
actions: [resetRetryCountAction] | |
} | |
] | |
} | |
}, | |
[RequestingToResumeTrackState]: { | |
invoke: { | |
id: 'invokePauseTrack', | |
src: resumeTrackService, | |
onDone: { | |
target: PlayingState, | |
actions: [resetRetryCountAction] | |
}, | |
onError: [ | |
{ | |
target: RequestingToResumeTrackState, | |
actions: [updateRetryCountAction], | |
cond: keepRetryingGuard | |
}, | |
{ | |
target: FailedToPlayState, | |
actions: [resetRetryCountAction] | |
} | |
] | |
} | |
}, | |
[PlayingState]: { | |
invoke: { | |
id: getPlaybackStateCallback, | |
src: (ctx) => (callback) => { | |
async function getInformationAboutTheUsersCurrentPlayback() { | |
let { player, loggerService } = ctx; | |
let state = await player.getCurrentState(); | |
if (state === null) { | |
return loggerService.log('No state obtained from the player. User is not playing music through the Web Playback SDK', 'error'); | |
} | |
callback({ type: PlayerStateChangedEvent, state }); | |
} | |
const id = setInterval(getInformationAboutTheUsersCurrentPlayback, 1000); | |
return () => clearInterval(id); | |
} | |
} | |
}, | |
[FailedToPlayState]: {}, | |
[RequestingToSkipPlaybackToPreviousTrackState]: { | |
invoke: { | |
id: 'skipPlaybackToPrevious', | |
src: skipPlaybackToPreviousTrackService, | |
onDone: { | |
target: PlayingState, | |
actions: [resetRetryCountAction] | |
}, | |
onError: [ | |
{ | |
target: PlayingState, | |
actions: [updateRetryCountAction], | |
cond: keepRetryingGuard | |
}, | |
{ | |
target: PlayingState, | |
actions: [resetRetryCountAction] | |
} | |
] | |
} | |
}, | |
[RequestingToSkipPlaybackToNextTrackState]: { | |
invoke: { | |
id: 'skipPlaybackToNext', | |
src: skipPlaybackToNextTrackService, | |
onDone: { | |
target: PlayingState, | |
actions: [resetRetryCountAction] | |
}, | |
onError: [ | |
{ | |
target: PlayingState, | |
actions: [updateRetryCountAction], | |
cond: keepRetryingGuard | |
}, | |
{ | |
target: PlayingState, | |
actions: [resetRetryCountAction] | |
} | |
] | |
} | |
} | |
}, | |
on: { | |
[TrackSelectedEvent]: { | |
target: PlayState, | |
actions: [setTrackSelectionAction, resetRetryCountAction], | |
cond: isNotTheSameTrackThatIsPlaying | |
}, | |
[AlbumSelectedEvent]: { | |
target: `${PlayState}.${RequestingToPlayAlbumState}`, | |
actions: [setAlbumSelectionAction, resetRetryCountAction] | |
}, | |
[TrackPausedEvent]: { | |
target: PauseState | |
}, | |
[TrackStoppedEvent]: { | |
target: StoppingState, | |
actions: [setPlayerWasBeingPlayingBeforeStoppingFlagAction] | |
}, | |
[PreviousTrackRequestedEvent]: [ | |
{ | |
target: `${PlayState}.${RequestingToSkipPlaybackToPreviousTrackState}`, | |
actions: [resetRetryCountAction], | |
cond: lessThanTwoSecondsHasPassedGuard | |
}, | |
{ | |
target: PlayState, | |
actions: [resetRetryCountAction] | |
} | |
], | |
[NextTrackRequestedEvent]: { | |
target: `${PlayState}.${RequestingToSkipPlaybackToNextTrackState}`, | |
actions: [resetRetryCountAction] | |
}, | |
[PlayerStateChangedEvent]: [ | |
{ | |
target: `${PauseState}.${PausedState}`, | |
actions: [logPlayerStateChangeAction, setPlaybackStateAction], | |
cond: playbackIsPausedGuard | |
}, | |
{ | |
actions: [logPlayerStateChangeAction, setPlaybackStateAction] | |
} | |
] | |
} | |
}, | |
[PauseState]: { | |
initial: RequestingToPauseTrackState, | |
states: { | |
[RequestingToPauseTrackState]: { | |
invoke: { | |
id: 'pauseTrack', | |
src: pauseTrackService, | |
onDone: { | |
target: PausedState, | |
actions: [resetRetryCountAction] | |
}, | |
onError: [ | |
{ | |
target: RequestingToPauseTrackState, | |
actions: [updateRetryCountAction], | |
cond: keepRetryingGuard | |
}, | |
{ | |
actions: [send(FailedToPauseEvent), resetRetryCountAction] | |
} | |
] | |
} | |
}, | |
[PausedState]: {}, | |
[ResettingCurrentTrackProgressState]: { | |
invoke: { | |
id: 'resetCurrentTrackProgress', | |
src: resetCurrentTrackProgressService, | |
onDone: { | |
actions: [resetRetryCountAction] | |
}, | |
onError: [ | |
{ | |
target: ResettingCurrentTrackProgressState, | |
actions: [updateRetryCountAction], | |
cond: keepRetryingGuard | |
}, | |
{ | |
target: PausedState, | |
actions: [resetRetryCountAction] | |
} | |
] | |
} | |
} | |
}, | |
on: { | |
[FailedToPauseEvent]: `${PlayState}.${PlayingState}`, | |
[TrackSelectedEvent]: { | |
target: PlayState, | |
actions: [setTrackSelectionAction, resetRetryCountAction], | |
cond: isNotTheSameTrackThatIsPlaying | |
}, | |
[AlbumSelectedEvent]: { | |
target: `${PlayState}.${RequestingToPlayAlbumState}`, | |
actions: [setAlbumSelectionAction, resetRetryCountAction] | |
}, | |
[TrackResumedEvent]: { | |
target: `${PlayState}.${RequestingToResumeTrackState}`, | |
actions: [resetRetryCountAction] | |
}, | |
[TrackStoppedEvent]: { | |
target: StoppingState, | |
actions: [setPlayerWasBeingPausedBeforeStoppingFlagAction] | |
}, | |
[PreviousTrackRequestedEvent]: [ | |
{ | |
target: `${PlayState}.${RequestingToSkipPlaybackToPreviousTrackState}`, | |
actions: [resetRetryCountAction], | |
cond: lessThanTwoSecondsHasPassedGuard | |
}, | |
{ | |
target: `${PauseState}.${ResettingCurrentTrackProgressState}`, | |
actions: [resetRetryCountAction] | |
} | |
], | |
[NextTrackRequestedEvent]: { | |
target: `${PlayState}.${RequestingToSkipPlaybackToNextTrackState}`, | |
actions: [resetRetryCountAction] | |
}, | |
[PlayerStateChangedEvent]: [ | |
{ | |
target: `${PlayState}.${PlayingState}`, | |
actions: [logPlayerStateChangeAction, setPlaybackStateAction], | |
cond: playbackIsNotPausedGuard | |
}, | |
{ | |
actions: [logPlayerStateChangeAction, setPlaybackStateAction] | |
} | |
] | |
} | |
}, | |
[StoppingState]: { | |
exit: [ | |
deletePlayerWasBeingPlayingBeforeStoppingFlagAction, | |
deletePlayerWasBeingPausedBeforeStoppingFlagAction | |
], | |
invoke: { | |
id: 'stopTrack', | |
src: stopTrackService, | |
onDone: { | |
target: `${PauseState}.${PausedState}`, | |
actions: [resetRetryCountAction] | |
}, | |
onError: [ | |
{ | |
target: StoppingState, | |
actions: [updateRetryCountAction], | |
cond: keepRetryingGuard | |
}, | |
{ | |
target: `${PlayState}.${PlayingState}`, | |
actions: [send(FailedToStopEvent), resetRetryCountAction], | |
cond: wasPlayingBeforeStoppingGuard | |
}, | |
{ | |
target: `${PauseState}.${PausedState}`, | |
actions: [send(FailedToStopEvent), resetRetryCountAction], | |
cond: wasPausedBeforeStoppingGuard | |
} | |
] | |
}, | |
on: { | |
[TrackSelectedEvent]: { | |
target: PlayState, | |
actions: [setTrackSelectionAction], | |
cond: isNotTheSameTrackThatIsPlaying | |
}, | |
[AlbumSelectedEvent]: { | |
target: `${PlayState}.${RequestingToPlayAlbumState}`, | |
actions: [setAlbumSelectionAction, resetRetryCountAction] | |
}, | |
[FailedToStopEvent]: [ | |
{ | |
target: `${PlayState}.${PlayingState}`, | |
cond: wasPlayingBeforeStoppingGuard | |
}, | |
{ | |
target: `${PauseState}.${PausedState}`, | |
cond: wasPausedBeforeStoppingGuard | |
} | |
], | |
[PreviousTrackRequestedEvent]: {}, | |
[NextTrackRequestedEvent]: {} | |
} | |
} | |
}, | |
on: { | |
[PlaybackErrorOcurredEvent]: PlaybackErrorState, | |
[PlayerNotReadyEvent]: { | |
target: NotReadyState, | |
actions: [logPlayerNotReadyAction] | |
} | |
} | |
}, | |
[NotReadyState]: { | |
entry: [logPlayerNotReadyAction], | |
on: { | |
[PlayerReadyEvent]: { | |
target: ReadyState, | |
actions: [logPlayerReadyAction, saveDeviceIdAction] | |
} | |
} | |
}, | |
[InitializationErrorState]: { | |
entry: [logInitializationErrorAction] | |
}, | |
[AuthenticationErrorState]: { | |
entry: [logAuthenticationErrorAction] | |
}, | |
[AccountErrorState]: { | |
entry: [logAccountErrorAction] | |
}, | |
[PlaybackErrorState]: { | |
entry: [logPlaybackErrorAction] | |
} | |
}, | |
on: { | |
[InitializationErrorOcurredEvent]: InitializationErrorState, | |
[AuthenticationErrorOcurredEvent]: AuthenticationErrorState, | |
[AccountErrorOcurredEvent]: AccountErrorState | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment