<commit_before>import orchestrate from './index'
import { createStore, applyMiddleware } from 'redux'

function getActions(config, options, dispatcher) {
  const actions = []
  const reducer = (state = {testState: 'testState'}, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))

  dispatcher(store.dispatch)

  return actions
}

function getLastAction(config, options, testAction) {
  const actions = getActions(config, options, function (dispatch) {
    dispatch(testAction)
  })
  return actions[actions.length - 1]
}
it('should throw an error', () => {
  expect(getLastAction).toThrow()
})

it('should dispatch action', () => {
  const testAction = { type: 'TEST' }
  const options = { validate: false }
  const config = []

  expect(getLastAction(config, options, testAction))
    .toEqual(testAction)
})

it('should not dispatch action', () => {
  const testAction = { type: 'TEST' }
  const options = { validate: true }
  const config = []

  expect(getLastAction(config, options, testAction))
    .not.toEqual(testAction)
})

it('should transform actions', () => {
  const testAction = { type: 'TEST' }
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'AFTER_ORCHESTRATION'
    }
  ]

  expect(getLastAction(config, options, testAction))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })
})

it('should transform actions - case array format', () => {
  const options = { validate: true }
  const config = [
    {
      case: ['TEST', 'TEST2'],
      dispatch: 'AFTER_ORCHESTRATION'
    }
  ]

  expect(getLastAction(config, options, { type: 'TEST' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })

  expect(getLastAction(config, options, { type: 'TEST2' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })
})

it('should transform actions - cascade', () => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'AFTER_ORCHESTRATION'
    },
    {
      case: 'AFTER_ORCHESTRATION',
      dispatch: 'AFTER_AFTER_ORCHESTRATION'
    }
  ]

  expect(getLastAction(config, options, { type: 'TEST' }))
    .toEqual({ type: 'AFTER_AFTER_ORCHESTRATION' })
})

it('should transform actions - dispatch object', () => {
  const options = { validate: true }
  const config = [
    {
      case: ['TEST', 'TEST2'],
      dispatch: { type: 'AFTER_ORCHESTRATION' }
    }
  ]

  expect(getLastAction(config, options, { type: 'TEST' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })

  expect(getLastAction(config, options, { type: 'TEST2' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })
})

it('should transform actions - dispatch function', () => {
  const testAction = { type: 'TEST' }
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: (a, s) => `AFTER_ORCHESTRATION_${a.type}_${s.testState}`
    }
  ]

  expect(getLastAction(config, options, testAction))
    .toEqual({ type: 'AFTER_ORCHESTRATION_TEST_testState' })
})

it('should transform actions - multiple rules', () => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'AFTER_ORCHESTRATION'
    },
    {
      case: 'TEST2',
      dispatch: 'AFTER_ORCHESTRATION2'
    }
  ]

  expect(getLastAction(config, options, { type: 'TEST' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })

  expect(getLastAction(config, options, { type: 'TEST2' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION2' })
})

it('should transform actions - delay', (done) => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'DEBOUNCED',
      delay: 50
    }
  ]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'TEST' })

  if (actions.length !== 1) {
    done.fail(`expected 1 dispatched action, got ${actions.length}`)
  }

  setTimeout(() => {
    if (actions.length === 2) {
      done()
    } else {
      done.fail(`expected 2 dispatched actions, got ${actions.length}`)
    }
  }, 100)
})

it('should transform actions - debounce', (done) => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'DEBOUNCED',
      debounce: 50
    },
    {
      case: 'TEST2',
      dispatch: 'DEBOUNCED',
      debounce: 50
    }
  ]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))

  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST2' })
  store.dispatch({ type: 'TEST2' })
  store.dispatch({ type: 'TEST2' })

  setTimeout(() => {
    const expectedNum = 3
    if (actions.length === expectedNum) {
      done()
    } else {
      done.fail(`expected ${expectedNum} dispatched actions, got ${actions.length}`)
    }
  }, 200)
})

it('should transform actions - delay debounce', (done) => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'DEBOUNCED',
      delay: 50,
      debounce: 50
    }
  ]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))

  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })

  if (actions.length !== 1) {
    done.fail(`expected 1 dispatched action, got ${actions.length}`)
  }

  setTimeout(() => {
    const expectedNum = 2
    if (actions.length === expectedNum) {
      done()
    } else {
      done.fail(`expected ${expectedNum} dispatched actions, got ${actions.length}`)
    }
  }, 110)
})

it('should fail sending request', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      request: {
        url: 'https://non.existing.c',
        onSuccess: 'ADD_MESSAGE_SUCCEEDED',
        onFail: 'ADD_MESSAGE_FAILED',
        callback: () => {
          if (actions[1].type === 'ADD_MESSAGE_FAILED') {
            done()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })
})

it('should fail sending request - onFail function', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      request: {
        url: 'https://non.existing.c',
        onSuccess: 'ADD_MESSAGE_SUCCEEDED',
        onFail: err => ({ type: 'ADD_MESSAGE_FAILED' }),
        callback: () => {
          if (actions[1].type === 'ADD_MESSAGE_FAILED') {
            done()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })
})

it('should fail sending request - post', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      post: {
        url: 'https://non.existing.c',
        onSuccess: 'ADD_MESSAGE_SUCCEEDED',
        onFail: 'ADD_MESSAGE_FAILED',
        callback: (err) => {
          if (actions[1].type === 'ADD_MESSAGE_FAILED') {
            done()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })
})

/*
// commented because this test is depending on github api

it('should send request', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      request: {
        url: 'https://api.github.com/users/test',
        onSuccess: res => ({ type: 'ADD_MESSAGE_SUCCEEDED', payload: res.data }),
        onFail: 'ADD_MESSAGE_FAILED',
        callback: () => {
          if (actions[1].type === 'ADD_MESSAGE_SUCCEEDED' && actions[1].payload.login === 'test') {
            done()
          } else {
            done.fail()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })
})
*/
/*
it('should send request - onSuccess string', (done) => {
  // this test is depending on github api
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      request: {
        url: 'https://api.github.com/users/test',
        onSuccess: 'ADD_MESSAGE_SUCCEEDED',
        onFail: 'ADD_MESSAGE_FAILED',
        callback: () => {
          if (actions[1].type === 'ADD_MESSAGE_SUCCEEDED') {
            done()
          } else {
            done.fail()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })
})
*/
it('should transform actions - cascade debounce', (done) => {
  const options = { validate: true }
  const config = [
    {
      case: [
			  'CHAT_INPUT_SUBMITTED',
			  'MESSANGER_INPUT_SUBMITED'
		  ],
      dispatch: 'ADD_MESSAGE_REQUESTED'
    },
    {
      case: 'ADD_MESSAGE_REQUESTED',
      dispatch: 'SECOND',
      debounce: 100
    }
  ]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'CHAT_INPUT_SUBMITTED' })
  store.dispatch({ type: 'MESSANGER_INPUT_SUBMITED' })
  store.dispatch({ type: 'CHAT_INPUT_SUBMITTED' })

  setTimeout(() => {
    if (
      actions[1].type === 'ADD_MESSAGE_REQUESTED',
      actions[2].type === 'ADD_MESSAGE_REQUESTED',
      actions[3].type === 'ADD_MESSAGE_REQUESTED',
      actions[4].type === 'SECOND'
    ) {
      done()
    } else {
      done.fail()
    }
  }, 200)

})

it('should fail sending request - debounce', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: [
			  'CHAT_INPUT_SUBMITTED',
			  'MESSANGER_INPUT_SUBMITED'
		  ],
      dispatch: 'ADD_MESSAGE_REQUESTED'
    },
    {
      case: 'ADD_MESSAGE_REQUESTED',
      debounce: 200,
      request: {
        url: 'https://non.existing.c',
        onSuccess: 'ADD_MESSAGE_SUCCEEDED',
        onFail: 'ADD_MESSAGE_FAILED',
        callback: () => {
          if (
            actions[1].type === 'ADD_MESSAGE_REQUESTED',
            actions[2].type === 'ADD_MESSAGE_REQUESTED',
            actions[3].type === 'ADD_MESSAGE_REQUESTED',
            actions[4].type === 'ADD_MESSAGE_FAILED'
          ) {
            done()
          } else {
            done.fail()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'CHAT_INPUT_SUBMITTED' })
  store.dispatch({ type: 'MESSANGER_INPUT_SUBMITED' })
  store.dispatch({ type: 'CHAT_INPUT_SUBMITTED' })
})

it('should cancel sending request', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      request: {
        url: 'https://jsonplaceholder.typicode.com/posts/1',
        onFail: 'ON_FAIL',
        onSuccess: 'ON_SUCCESS',
        cancelWhen: [
          'CANCEL_EVENT',
          'CANCEL_EVENT_SECOND'
        ],
        callback: (err) => {
          if (err.message.type === 'CANCEL_EVENT') {
            done()
          } else {
            done.fail()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })

  setTimeout(() => {
    store.dispatch({ type: 'CANCEL_EVENT' })
  }, 0)
})

it('should dispatch multiple actions', (done) => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: ['FIRST', 'SECOND']
    }
  ]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'TEST' })

  setTimeout(() => {
    if (actions.length === 3) {
      done()
    } else {
      done.fail()
    }
  }, 100)
})

it('should dispatch added actions from dynamically added rules', (done) => {
  const options = { validate: true }
  const rules = [{
    case: 'TEST',
    dispatch: ['FIRST', 'SECOND']
  }]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate([], options)))

  orchestrate.addRules(rules);
  store.dispatch({ type: 'TEST' })

  setTimeout(() => {
    if (actions.length === 3) {
      done()
    } else {
      done.fail()
    }
  }, 100)
})

<commit_msg>Add extra test<commit_after>import orchestrate from './index'
import { createStore, applyMiddleware } from 'redux'

function getActions(config, options, dispatcher) {
  const actions = []
  const reducer = (state = {testState: 'testState'}, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))

  dispatcher(store.dispatch)

  return actions
}

function getLastAction(config, options, testAction) {
  const actions = getActions(config, options, function (dispatch) {
    dispatch(testAction)
  })
  return actions[actions.length - 1]
}
it('should throw an error', () => {
  expect(getLastAction).toThrow()
})

it('should dispatch action', () => {
  const testAction = { type: 'TEST' }
  const options = { validate: false }
  const config = []

  expect(getLastAction(config, options, testAction))
    .toEqual(testAction)
})

it('should not dispatch action', () => {
  const testAction = { type: 'TEST' }
  const options = { validate: true }
  const config = []

  expect(getLastAction(config, options, testAction))
    .not.toEqual(testAction)
})

it('should transform actions', () => {
  const testAction = { type: 'TEST' }
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'AFTER_ORCHESTRATION'
    }
  ]

  expect(getLastAction(config, options, testAction))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })
})

it('should transform actions - case array format', () => {
  const options = { validate: true }
  const config = [
    {
      case: ['TEST', 'TEST2'],
      dispatch: 'AFTER_ORCHESTRATION'
    }
  ]

  expect(getLastAction(config, options, { type: 'TEST' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })

  expect(getLastAction(config, options, { type: 'TEST2' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })
})

it('should transform actions - cascade', () => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'AFTER_ORCHESTRATION'
    },
    {
      case: 'AFTER_ORCHESTRATION',
      dispatch: 'AFTER_AFTER_ORCHESTRATION'
    }
  ]

  expect(getLastAction(config, options, { type: 'TEST' }))
    .toEqual({ type: 'AFTER_AFTER_ORCHESTRATION' })
})

it('should transform actions - dispatch object', () => {
  const options = { validate: true }
  const config = [
    {
      case: ['TEST', 'TEST2'],
      dispatch: { type: 'AFTER_ORCHESTRATION' }
    }
  ]

  expect(getLastAction(config, options, { type: 'TEST' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })

  expect(getLastAction(config, options, { type: 'TEST2' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })
})

it('should transform actions - dispatch function', () => {
  const testAction = { type: 'TEST' }
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: (a, s) => `AFTER_ORCHESTRATION_${a.type}_${s.testState}`
    }
  ]

  expect(getLastAction(config, options, testAction))
    .toEqual({ type: 'AFTER_ORCHESTRATION_TEST_testState' })
})

it('should transform actions - multiple rules', () => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'AFTER_ORCHESTRATION'
    },
    {
      case: 'TEST2',
      dispatch: 'AFTER_ORCHESTRATION2'
    }
  ]

  expect(getLastAction(config, options, { type: 'TEST' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION' })

  expect(getLastAction(config, options, { type: 'TEST2' }))
    .toEqual({ type: 'AFTER_ORCHESTRATION2' })
})

it('should transform actions - delay', (done) => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'DEBOUNCED',
      delay: 50
    }
  ]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'TEST' })

  if (actions.length !== 1) {
    done.fail(`expected 1 dispatched action, got ${actions.length}`)
  }

  setTimeout(() => {
    if (actions.length === 2) {
      done()
    } else {
      done.fail(`expected 2 dispatched actions, got ${actions.length}`)
    }
  }, 100)
})

it('should transform actions - debounce', (done) => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'DEBOUNCED',
      debounce: 50
    },
    {
      case: 'TEST2',
      dispatch: 'DEBOUNCED',
      debounce: 50
    }
  ]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))

  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST2' })
  store.dispatch({ type: 'TEST2' })
  store.dispatch({ type: 'TEST2' })

  setTimeout(() => {
    const expectedNum = 3
    if (actions.length === expectedNum) {
      done()
    } else {
      done.fail(`expected ${expectedNum} dispatched actions, got ${actions.length}`)
    }
  }, 200)
})

it('should transform actions - delay debounce', (done) => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: 'DEBOUNCED',
      delay: 50,
      debounce: 50
    }
  ]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))

  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })
  store.dispatch({ type: 'TEST' })

  if (actions.length !== 1) {
    done.fail(`expected 1 dispatched action, got ${actions.length}`)
  }

  setTimeout(() => {
    const expectedNum = 2
    if (actions.length === expectedNum) {
      done()
    } else {
      done.fail(`expected ${expectedNum} dispatched actions, got ${actions.length}`)
    }
  }, 110)
})

it('should fail sending request', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      request: {
        url: 'https://non.existing.c',
        onSuccess: 'ADD_MESSAGE_SUCCEEDED',
        onFail: 'ADD_MESSAGE_FAILED',
        callback: () => {
          if (actions[1].type === 'ADD_MESSAGE_FAILED') {
            done()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })
})

it('should fail sending request - onFail function', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      request: {
        url: 'https://non.existing.c',
        onSuccess: 'ADD_MESSAGE_SUCCEEDED',
        onFail: err => ({ type: 'ADD_MESSAGE_FAILED' }),
        callback: () => {
          if (actions[1].type === 'ADD_MESSAGE_FAILED') {
            done()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })
})

it('should fail sending request - post', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      post: {
        url: 'https://non.existing.c',
        onSuccess: 'ADD_MESSAGE_SUCCEEDED',
        onFail: 'ADD_MESSAGE_FAILED',
        callback: (err) => {
          if (actions[1].type === 'ADD_MESSAGE_FAILED') {
            done()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })
})

/*
// commented because this test is depending on github api

it('should send request', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      request: {
        url: 'https://api.github.com/users/test',
        onSuccess: res => ({ type: 'ADD_MESSAGE_SUCCEEDED', payload: res.data }),
        onFail: 'ADD_MESSAGE_FAILED',
        callback: () => {
          if (actions[1].type === 'ADD_MESSAGE_SUCCEEDED' && actions[1].payload.login === 'test') {
            done()
          } else {
            done.fail()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })
})
*/
/*
it('should send request - onSuccess string', (done) => {
  // this test is depending on github api
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      request: {
        url: 'https://api.github.com/users/test',
        onSuccess: 'ADD_MESSAGE_SUCCEEDED',
        onFail: 'ADD_MESSAGE_FAILED',
        callback: () => {
          if (actions[1].type === 'ADD_MESSAGE_SUCCEEDED') {
            done()
          } else {
            done.fail()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })
})
*/
it('should transform actions - cascade debounce', (done) => {
  const options = { validate: true }
  const config = [
    {
      case: [
			  'CHAT_INPUT_SUBMITTED',
			  'MESSANGER_INPUT_SUBMITED'
		  ],
      dispatch: 'ADD_MESSAGE_REQUESTED'
    },
    {
      case: 'ADD_MESSAGE_REQUESTED',
      dispatch: 'SECOND',
      debounce: 100
    }
  ]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'CHAT_INPUT_SUBMITTED' })
  store.dispatch({ type: 'MESSANGER_INPUT_SUBMITED' })
  store.dispatch({ type: 'CHAT_INPUT_SUBMITTED' })

  setTimeout(() => {
    if (
      actions[1].type === 'ADD_MESSAGE_REQUESTED',
      actions[2].type === 'ADD_MESSAGE_REQUESTED',
      actions[3].type === 'ADD_MESSAGE_REQUESTED',
      actions[4].type === 'SECOND'
    ) {
      done()
    } else {
      done.fail()
    }
  }, 200)

})

it('should fail sending request - debounce', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: [
			  'CHAT_INPUT_SUBMITTED',
			  'MESSANGER_INPUT_SUBMITED'
		  ],
      dispatch: 'ADD_MESSAGE_REQUESTED'
    },
    {
      case: 'ADD_MESSAGE_REQUESTED',
      debounce: 200,
      request: {
        url: 'https://non.existing.c',
        onSuccess: 'ADD_MESSAGE_SUCCEEDED',
        onFail: 'ADD_MESSAGE_FAILED',
        callback: () => {
          if (
            actions[1].type === 'ADD_MESSAGE_REQUESTED',
            actions[2].type === 'ADD_MESSAGE_REQUESTED',
            actions[3].type === 'ADD_MESSAGE_REQUESTED',
            actions[4].type === 'ADD_MESSAGE_FAILED'
          ) {
            done()
          } else {
            done.fail()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'CHAT_INPUT_SUBMITTED' })
  store.dispatch({ type: 'MESSANGER_INPUT_SUBMITED' })
  store.dispatch({ type: 'CHAT_INPUT_SUBMITTED' })
})

it('should cancel sending request', (done) => {
  const actions = []
  const options = { validate: true }
  const config = [
    {
      case: 'ADD_MESSAGE_REQUESTED',
      request: {
        url: 'https://jsonplaceholder.typicode.com/posts/1',
        onFail: 'ON_FAIL',
        onSuccess: 'ON_SUCCESS',
        cancelWhen: [
          'CANCEL_EVENT',
          'CANCEL_EVENT_SECOND'
        ],
        callback: (err) => {
          if (err.message.type === 'CANCEL_EVENT') {
            done()
          } else {
            done.fail()
          }
        }
      }
    }
  ]

  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'ADD_MESSAGE_REQUESTED' })

  setTimeout(() => {
    store.dispatch({ type: 'CANCEL_EVENT' })
  }, 0)
})

it('should dispatch multiple actions', (done) => {
  const options = { validate: true }
  const config = [
    {
      case: 'TEST',
      dispatch: ['FIRST', 'SECOND']
    }
  ]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate(config, options)))
  store.dispatch({ type: 'TEST' })

  setTimeout(() => {
    if (actions.length === 3) {
      done()
    } else {
      done.fail()
    }
  }, 100)
})

it('should add rules dynamically', () => {
  const rules = [{
    case: 'TEST',
    dispatch: ['FIRST', 'SECOND']
  }]

  const reducer = (state, action) => {
    return state
  }

  const store = createStore(reducer, applyMiddleware(orchestrate([])))
  orchestrate.addRules(rules);

  expect(orchestrate.config.length).toEqual(1)
})

it('should dispatch added actions from dynamically added rules', (done) => {
  const options = { validate: true }
  const rules = [{
    case: 'TEST',
    dispatch: ['FIRST', 'SECOND']
  }]

  const actions = []
  const reducer = (state, action) => {
    actions.push(action)
    return state
  }
  const store = createStore(reducer, applyMiddleware(orchestrate([], options)))

  orchestrate.addRules(rules);
  store.dispatch({ type: 'TEST' })

  setTimeout(() => {
    if (actions.length === 3) {
      done()
    } else {
      done.fail()
    }
  }, 100)
})

<|endoftext|>


<commit_before>import difference from 'lodash/difference';

export const ActionTypes = {
  PERFORM_ACTION: 'PERFORM_ACTION',
  RESET: 'RESET',
  ROLLBACK: 'ROLLBACK',
  COMMIT: 'COMMIT',
  SWEEP: 'SWEEP',
  TOGGLE_ACTION: 'TOGGLE_ACTION',
  JUMP_TO_STATE: 'JUMP_TO_STATE',
  IMPORT_STATE: 'IMPORT_STATE'
};

/**
 * Action creators to change the History state.
 */
export const ActionCreators = {
  performAction(action) {
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
        'Have you misspelled a constant?'
      );
    }
    return { type: ActionTypes.PERFORM_ACTION, action, timestamp: Date.now() };
  },

  reset() {
    return { type: ActionTypes.RESET, timestamp: Date.now() };
  },

  rollback() {
    return { type: ActionTypes.ROLLBACK, timestamp: Date.now() };
  },

  commit() {
    return { type: ActionTypes.COMMIT, timestamp: Date.now() };
  },

  sweep() {
    return { type: ActionTypes.SWEEP };
  },

  toggleAction(id) {
    return { type: ActionTypes.TOGGLE_ACTION, id };
  },

  jumpToState(index) {
    return { type: ActionTypes.JUMP_TO_STATE, index };
  },

  importState(nextLiftedState) {
    return { type: ActionTypes.IMPORT_STATE, nextLiftedState };
  }
};

const INIT_ACTION = { type: '@@INIT' };

/**
 * Computes the next entry in the log by applying an action.
 */
function computeNextEntry(reducer, action, state, error) {
  if (error) {
    return {
      state,
      error: 'Interrupted by an error up the chain'
    };
  }

  let nextState = state;
  let nextError;
  try {
    nextState = reducer(state, action);
  } catch (err) {
    nextError = err.toString();
    if (typeof window === 'object' && typeof window.chrome !== 'undefined') {
      // In Chrome, rethrowing provides better source map support
      setTimeout(() => { throw err; });
    } else {
      console.error(err.stack || err);
    }
  }

  return {
    state: nextState,
    error: nextError
  };
}

/**
 * Runs the reducer on invalidated actions to get a fresh computation log.
 */
function recomputeStates(
  computedStates,
  minInvalidatedStateIndex,
  reducer,
  committedState,
  actionsById,
  stagedActionIds,
  skippedActionIds
) {
  // Optimization: exit early and return the same reference
  // if we know nothing could have changed.
  if (
    minInvalidatedStateIndex >= computedStates.length &&
    computedStates.length === stagedActionIds.length
  ) {
    return computedStates;
  }

  const nextComputedStates = computedStates.slice(0, minInvalidatedStateIndex);
  for (let i = minInvalidatedStateIndex; i < stagedActionIds.length; i++) {
    const actionId = stagedActionIds[i];
    const action = actionsById[actionId].action;

    const previousEntry = nextComputedStates[i - 1];
    const previousState = previousEntry ? previousEntry.state : committedState;
    const previousError = previousEntry ? previousEntry.error : undefined;

    const shouldSkip = skippedActionIds.indexOf(actionId) > -1;
    const entry = shouldSkip ?
      previousEntry :
      computeNextEntry(reducer, action, previousState, previousError);

    nextComputedStates.push(entry);
  }

  return nextComputedStates;
}

/**
 * Lifts an app's action into an action on the lifted store.
 */
function liftAction(action) {
  return ActionCreators.performAction(action);
}

/**
 * Creates a history state reducer from an app's reducer.
 */
function liftReducerWith(reducer, initialCommittedState, monitorReducer, options) {
  const initialLiftedState = {
    monitorState: monitorReducer(undefined, {}),
    nextActionId: 1,
    actionsById: { 0: liftAction(INIT_ACTION) },
    stagedActionIds: [0],
    skippedActionIds: [],
    committedState: initialCommittedState,
    currentStateIndex: 0,
    computedStates: []
  };

  /**
   * Manages how the history actions modify the history state.
   */
  return (liftedState = initialLiftedState, liftedAction) => {
    let {
      monitorState,
      actionsById,
      nextActionId,
      stagedActionIds,
      skippedActionIds,
      committedState,
      currentStateIndex,
      computedStates
    } = liftedState;

    // By default, agressively recompute every state whatever happens.
    // This has O(n) performance, so we'll override this to a sensible
    // value whenever we feel like we don't have to recompute the states.
    let minInvalidatedStateIndex = 0;

    switch (liftedAction.type) {
      case ActionTypes.RESET: {
        // Get back to the state the store was created with.
        actionsById = { 0: liftAction(INIT_ACTION) };
        nextActionId = 1;
        stagedActionIds = [0];
        skippedActionIds = [];
        committedState = initialCommittedState;
        currentStateIndex = 0;
        computedStates = [];
        break;
      }
      case ActionTypes.COMMIT: {
        // Consider the last committed state the new starting point.
        // Squash any staged actions into a single committed state.
        actionsById = { 0: liftAction(INIT_ACTION) };
        nextActionId = 1;
        stagedActionIds = [0];
        skippedActionIds = [];
        committedState = computedStates[currentStateIndex].state;
        currentStateIndex = 0;
        computedStates = [];
        break;
      }
      case ActionTypes.ROLLBACK: {
        // Forget about any staged actions.
        // Start again from the last committed state.
        actionsById = { 0: liftAction(INIT_ACTION) };
        nextActionId = 1;
        stagedActionIds = [0];
        skippedActionIds = [];
        currentStateIndex = 0;
        computedStates = [];
        break;
      }
      case ActionTypes.TOGGLE_ACTION: {
        // Toggle whether an action with given ID is skipped.
        // Being skipped means it is a no-op during the computation.
        const { id: actionId } = liftedAction;
        const index = skippedActionIds.indexOf(actionId);
        if (index === -1) {
          skippedActionIds = [actionId, ...skippedActionIds];
        } else {
          skippedActionIds = skippedActionIds.filter(id => id !== actionId);
        }
        // Optimization: we know history before this action hasn't changed
        minInvalidatedStateIndex = stagedActionIds.indexOf(actionId);
        break;
      }
      case ActionTypes.JUMP_TO_STATE: {
        // Without recomputing anything, move the pointer that tell us
        // which state is considered the current one. Useful for sliders.
        currentStateIndex = liftedAction.index;
        // Optimization: we know the history has not changed.
        minInvalidatedStateIndex = Infinity;
        break;
      }
      case ActionTypes.SWEEP: {
        // Forget any actions that are currently being skipped.
        stagedActionIds = difference(stagedActionIds, skippedActionIds);
        skippedActionIds = [];
        currentStateIndex = Math.min(currentStateIndex, stagedActionIds.length - 1);
        break;
      }
      case ActionTypes.PERFORM_ACTION: {
        if (options.maxAge && stagedActionIds.length === options.maxAge) {
          // If maxAge has been reached, auto-commit earliest non-@@INIT action.
          delete actionsById[stagedActionIds[1]];
          skippedActionIds = skippedActionIds.filter(id => id !== stagedActionIds[1]);
          stagedActionIds = [0].concat(stagedActionIds.slice(2));
          committedState = computedStates[1].state;
          computedStates = computedStates.slice(1);
        }
        if (currentStateIndex === stagedActionIds.length - 1) {
          currentStateIndex++;
        }
        const actionId = nextActionId++;
        // Mutation! This is the hottest path, and we optimize on purpose.
        // It is safe because we set a new key in a cache dictionary.
        actionsById[actionId] = liftedAction;
        stagedActionIds = [...stagedActionIds, actionId];
        // Optimization: we know that only the new action needs computing.
        minInvalidatedStateIndex = stagedActionIds.length - 1;
        break;
      }
      case ActionTypes.IMPORT_STATE: {
        // Completely replace everything.
        ({
          monitorState,
          actionsById,
          nextActionId,
          stagedActionIds,
          skippedActionIds,
          committedState,
          currentStateIndex,
          computedStates
        } = liftedAction.nextLiftedState);
        break;
      }
      case '@@redux/INIT': {
        // Always recompute states on hot reload and init.
        minInvalidatedStateIndex = 0;
        break;
      }
      default: {
        // If the action is not recognized, it's a monitor action.
        // Optimization: a monitor action can't change history.
        minInvalidatedStateIndex = Infinity;
        break;
      }
    }

    computedStates = recomputeStates(
      computedStates,
      minInvalidatedStateIndex,
      reducer,
      committedState,
      actionsById,
      stagedActionIds,
      skippedActionIds
    );
    monitorState = monitorReducer(monitorState, liftedAction);
    return {
      monitorState,
      actionsById,
      nextActionId,
      stagedActionIds,
      skippedActionIds,
      committedState,
      currentStateIndex,
      computedStates
    };
  };
}

/**
 * Provides an app's view into the state of the lifted store.
 */
function unliftState(liftedState) {
  const { computedStates, currentStateIndex } = liftedState;
  const { state } = computedStates[currentStateIndex];
  return state;
}

/**
 * Provides an app's view into the lifted store.
 */
function unliftStore(liftedStore, liftReducer) {
  let lastDefinedState;

  return {
    ...liftedStore,

    liftedStore,

    dispatch(action) {
      liftedStore.dispatch(liftAction(action));
      return action;
    },

    getState() {
      const state = unliftState(liftedStore.getState());
      if (state !== undefined) {
        lastDefinedState = state;
      }
      return lastDefinedState;
    },

    replaceReducer(nextReducer) {
      liftedStore.replaceReducer(liftReducer(nextReducer));
    }
  };
}

/**
 * Redux instrumentation store enhancer.
 */
export default function instrument(monitorReducer = () => null, options = {}) {
  return createStore => (reducer, initialState, enhancer) => {

    function liftReducer(r) {
      if (typeof r !== 'function') {
        if (r && typeof r.default === 'function') {
          throw new Error(
            'Expected the reducer to be a function. ' +
            'Instead got an object with a "default" field. ' +
            'Did you pass a module instead of the default export? ' +
            'Try passing require(...).default instead.'
          );
        }
        throw new Error('Expected the reducer to be a function.');
      }
      return liftReducerWith(r, initialState, monitorReducer, options);
    }

    const liftedStore = createStore(liftReducer(reducer), enhancer);
    if (liftedStore.liftedStore) {
      throw new Error(
        'DevTools instrumentation should not be applied more than once. ' +
        'Check your store configuration.'
      );
    }

    return unliftStore(liftedStore, liftReducer);
  };
}
<commit_msg>don't auto-commit an error<commit_after>import difference from 'lodash/difference';

export const ActionTypes = {
  PERFORM_ACTION: 'PERFORM_ACTION',
  RESET: 'RESET',
  ROLLBACK: 'ROLLBACK',
  COMMIT: 'COMMIT',
  SWEEP: 'SWEEP',
  TOGGLE_ACTION: 'TOGGLE_ACTION',
  JUMP_TO_STATE: 'JUMP_TO_STATE',
  IMPORT_STATE: 'IMPORT_STATE'
};

/**
 * Action creators to change the History state.
 */
export const ActionCreators = {
  performAction(action) {
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
        'Have you misspelled a constant?'
      );
    }
    return { type: ActionTypes.PERFORM_ACTION, action, timestamp: Date.now() };
  },

  reset() {
    return { type: ActionTypes.RESET, timestamp: Date.now() };
  },

  rollback() {
    return { type: ActionTypes.ROLLBACK, timestamp: Date.now() };
  },

  commit() {
    return { type: ActionTypes.COMMIT, timestamp: Date.now() };
  },

  sweep() {
    return { type: ActionTypes.SWEEP };
  },

  toggleAction(id) {
    return { type: ActionTypes.TOGGLE_ACTION, id };
  },

  jumpToState(index) {
    return { type: ActionTypes.JUMP_TO_STATE, index };
  },

  importState(nextLiftedState) {
    return { type: ActionTypes.IMPORT_STATE, nextLiftedState };
  }
};

const INIT_ACTION = { type: '@@INIT' };

/**
 * Computes the next entry in the log by applying an action.
 */
function computeNextEntry(reducer, action, state, error) {
  if (error) {
    return {
      state,
      error: 'Interrupted by an error up the chain'
    };
  }

  let nextState = state;
  let nextError;
  try {
    nextState = reducer(state, action);
  } catch (err) {
    nextError = err.toString();
    if (typeof window === 'object' && typeof window.chrome !== 'undefined') {
      // In Chrome, rethrowing provides better source map support
      setTimeout(() => { throw err; });
    } else {
      console.error(err.stack || err);
    }
  }

  return {
    state: nextState,
    error: nextError
  };
}

/**
 * Runs the reducer on invalidated actions to get a fresh computation log.
 */
function recomputeStates(
  computedStates,
  minInvalidatedStateIndex,
  reducer,
  committedState,
  actionsById,
  stagedActionIds,
  skippedActionIds
) {
  // Optimization: exit early and return the same reference
  // if we know nothing could have changed.
  if (
    minInvalidatedStateIndex >= computedStates.length &&
    computedStates.length === stagedActionIds.length
  ) {
    return computedStates;
  }

  const nextComputedStates = computedStates.slice(0, minInvalidatedStateIndex);
  for (let i = minInvalidatedStateIndex; i < stagedActionIds.length; i++) {
    const actionId = stagedActionIds[i];
    const action = actionsById[actionId].action;

    const previousEntry = nextComputedStates[i - 1];
    const previousState = previousEntry ? previousEntry.state : committedState;
    const previousError = previousEntry ? previousEntry.error : undefined;

    const shouldSkip = skippedActionIds.indexOf(actionId) > -1;
    const entry = shouldSkip ?
      previousEntry :
      computeNextEntry(reducer, action, previousState, previousError);

    nextComputedStates.push(entry);
  }

  return nextComputedStates;
}

/**
 * Lifts an app's action into an action on the lifted store.
 */
function liftAction(action) {
  return ActionCreators.performAction(action);
}

/**
 * Creates a history state reducer from an app's reducer.
 */
function liftReducerWith(reducer, initialCommittedState, monitorReducer, options) {
  const initialLiftedState = {
    monitorState: monitorReducer(undefined, {}),
    nextActionId: 1,
    actionsById: { 0: liftAction(INIT_ACTION) },
    stagedActionIds: [0],
    skippedActionIds: [],
    committedState: initialCommittedState,
    currentStateIndex: 0,
    computedStates: []
  };

  /**
   * Manages how the history actions modify the history state.
   */
  return (liftedState = initialLiftedState, liftedAction) => {
    let {
      monitorState,
      actionsById,
      nextActionId,
      stagedActionIds,
      skippedActionIds,
      committedState,
      currentStateIndex,
      computedStates
    } = liftedState;

    // By default, agressively recompute every state whatever happens.
    // This has O(n) performance, so we'll override this to a sensible
    // value whenever we feel like we don't have to recompute the states.
    let minInvalidatedStateIndex = 0;

    switch (liftedAction.type) {
      case ActionTypes.RESET: {
        // Get back to the state the store was created with.
        actionsById = { 0: liftAction(INIT_ACTION) };
        nextActionId = 1;
        stagedActionIds = [0];
        skippedActionIds = [];
        committedState = initialCommittedState;
        currentStateIndex = 0;
        computedStates = [];
        break;
      }
      case ActionTypes.COMMIT: {
        // Consider the last committed state the new starting point.
        // Squash any staged actions into a single committed state.
        actionsById = { 0: liftAction(INIT_ACTION) };
        nextActionId = 1;
        stagedActionIds = [0];
        skippedActionIds = [];
        committedState = computedStates[currentStateIndex].state;
        currentStateIndex = 0;
        computedStates = [];
        break;
      }
      case ActionTypes.ROLLBACK: {
        // Forget about any staged actions.
        // Start again from the last committed state.
        actionsById = { 0: liftAction(INIT_ACTION) };
        nextActionId = 1;
        stagedActionIds = [0];
        skippedActionIds = [];
        currentStateIndex = 0;
        computedStates = [];
        break;
      }
      case ActionTypes.TOGGLE_ACTION: {
        // Toggle whether an action with given ID is skipped.
        // Being skipped means it is a no-op during the computation.
        const { id: actionId } = liftedAction;
        const index = skippedActionIds.indexOf(actionId);
        if (index === -1) {
          skippedActionIds = [actionId, ...skippedActionIds];
        } else {
          skippedActionIds = skippedActionIds.filter(id => id !== actionId);
        }
        // Optimization: we know history before this action hasn't changed
        minInvalidatedStateIndex = stagedActionIds.indexOf(actionId);
        break;
      }
      case ActionTypes.JUMP_TO_STATE: {
        // Without recomputing anything, move the pointer that tell us
        // which state is considered the current one. Useful for sliders.
        currentStateIndex = liftedAction.index;
        // Optimization: we know the history has not changed.
        minInvalidatedStateIndex = Infinity;
        break;
      }
      case ActionTypes.SWEEP: {
        // Forget any actions that are currently being skipped.
        stagedActionIds = difference(stagedActionIds, skippedActionIds);
        skippedActionIds = [];
        currentStateIndex = Math.min(currentStateIndex, stagedActionIds.length - 1);
        break;
      }
      case ActionTypes.PERFORM_ACTION: {
        if (
          options.maxAge &&
          stagedActionIds.length === options.maxAge &&
          !computedStates[1].error
        ) {
          // If maxAge has been reached, auto-commit earliest non-@@INIT action.
          delete actionsById[stagedActionIds[1]];
          skippedActionIds = skippedActionIds.filter(id => id !== stagedActionIds[1]);
          stagedActionIds = [0].concat(stagedActionIds.slice(2));
          committedState = computedStates[1].state;
          computedStates = computedStates.slice(1);
        } else if (currentStateIndex === stagedActionIds.length - 1) {
          currentStateIndex++;
        }
        const actionId = nextActionId++;
        // Mutation! This is the hottest path, and we optimize on purpose.
        // It is safe because we set a new key in a cache dictionary.
        actionsById[actionId] = liftedAction;
        stagedActionIds = [...stagedActionIds, actionId];
        // Optimization: we know that only the new action needs computing.
        minInvalidatedStateIndex = stagedActionIds.length - 1;
        break;
      }
      case ActionTypes.IMPORT_STATE: {
        // Completely replace everything.
        ({
          monitorState,
          actionsById,
          nextActionId,
          stagedActionIds,
          skippedActionIds,
          committedState,
          currentStateIndex,
          computedStates
        } = liftedAction.nextLiftedState);
        break;
      }
      case '@@redux/INIT': {
        // Always recompute states on hot reload and init.
        minInvalidatedStateIndex = 0;
        break;
      }
      default: {
        // If the action is not recognized, it's a monitor action.
        // Optimization: a monitor action can't change history.
        minInvalidatedStateIndex = Infinity;
        break;
      }
    }

    computedStates = recomputeStates(
      computedStates,
      minInvalidatedStateIndex,
      reducer,
      committedState,
      actionsById,
      stagedActionIds,
      skippedActionIds
    );
    monitorState = monitorReducer(monitorState, liftedAction);
    return {
      monitorState,
      actionsById,
      nextActionId,
      stagedActionIds,
      skippedActionIds,
      committedState,
      currentStateIndex,
      computedStates
    };
  };
}

/**
 * Provides an app's view into the state of the lifted store.
 */
function unliftState(liftedState) {
  const { computedStates, currentStateIndex } = liftedState;
  const { state } = computedStates[currentStateIndex];
  return state;
}

/**
 * Provides an app's view into the lifted store.
 */
function unliftStore(liftedStore, liftReducer) {
  let lastDefinedState;

  return {
    ...liftedStore,

    liftedStore,

    dispatch(action) {
      liftedStore.dispatch(liftAction(action));
      return action;
    },

    getState() {
      const state = unliftState(liftedStore.getState());
      if (state !== undefined) {
        lastDefinedState = state;
      }
      return lastDefinedState;
    },

    replaceReducer(nextReducer) {
      liftedStore.replaceReducer(liftReducer(nextReducer));
    }
  };
}

/**
 * Redux instrumentation store enhancer.
 */
export default function instrument(monitorReducer = () => null, options = {}) {
  return createStore => (reducer, initialState, enhancer) => {

    function liftReducer(r) {
      if (typeof r !== 'function') {
        if (r && typeof r.default === 'function') {
          throw new Error(
            'Expected the reducer to be a function. ' +
            'Instead got an object with a "default" field. ' +
            'Did you pass a module instead of the default export? ' +
            'Try passing require(...).default instead.'
          );
        }
        throw new Error('Expected the reducer to be a function.');
      }
      return liftReducerWith(r, initialState, monitorReducer, options);
    }

    const liftedStore = createStore(liftReducer(reducer), enhancer);
    if (liftedStore.liftedStore) {
      throw new Error(
        'DevTools instrumentation should not be applied more than once. ' +
        'Check your store configuration.'
      );
    }

    return unliftStore(liftedStore, liftReducer);
  };
}
<|endoftext|>


<commit_before>/**
* A configuration object that describes a route.
*/
interface RouteConfig {
  /**
  * The route pattern to match against incoming URL fragments, or an array of patterns.
  */
  route: string|string[];

  /**
  * A unique name for the route that may be used to identify the route when generating URL fragments.
  * Required when this route should support URL generation, such as with [[Router.generate]] or
  * the route-href custom attribute.
  */
  name?: string;

  /**
  * The moduleId of the view model that should be activated for this route.
  */
  moduleId?: string;

  /**
  * A URL fragment to redirect to when this route is matched.
  */
  redirect?: string;

  /**
  * A function that can be used to dynamically select the module or modules to activate.
  * The function is passed the current [[NavigationInstruction]], and should configure
  * instruction.config with the desired moduleId, viewPorts, or redirect.
  */
  navigationStrategy?: (instruction: NavigationInstruction) => Promise<void>|void;

  /**
  * The view ports to target when activating this route. If unspecified, the target moduleId is loaded
  * into the default viewPort (the viewPort with name 'default'). The viewPorts object should have keys
  * whose property names correspond to names used by <router-view> elements. The values should be objects
  * specifying the moduleId to load into that viewPort.
  */
  viewPorts?: any;

  /**
  * When specified, this route will be included in the [[Router.navigation]] nav model. Useful for
  * dynamically generating menus or other navigation elements. When a number is specified, that value
  * will be used as a sort order.
  */
  nav?: boolean|number;

  /**
  * The URL fragment to use in nav models. If unspecified, the [[RouteConfig.route]] will be used.
  * However, if the [[RouteConfig.route]] contains dynamic segments, this property must be specified.
  */
  href?: string;

  /**
  * The document title to set when this route is active.
  */
  title?: string;

  /**
  * Arbitrary data to attach to the route. This can be used to attached custom data needed by components
  * like pipeline steps and activated modules.
  */
  settings?: any;

  [x: string]: any;
}
<commit_msg>fix(router): add NavModel to RouteConfig interface<commit_after>/**
* A configuration object that describes a route.
*/
interface RouteConfig {
  /**
  * The route pattern to match against incoming URL fragments, or an array of patterns.
  */
  route: string|string[];

  /**
  * A unique name for the route that may be used to identify the route when generating URL fragments.
  * Required when this route should support URL generation, such as with [[Router.generate]] or
  * the route-href custom attribute.
  */
  name?: string;

  /**
  * The moduleId of the view model that should be activated for this route.
  */
  moduleId?: string;

  /**
  * A URL fragment to redirect to when this route is matched.
  */
  redirect?: string;

  /**
  * A function that can be used to dynamically select the module or modules to activate.
  * The function is passed the current [[NavigationInstruction]], and should configure
  * instruction.config with the desired moduleId, viewPorts, or redirect.
  */
  navigationStrategy?: (instruction: NavigationInstruction) => Promise<void>|void;

  /**
  * The view ports to target when activating this route. If unspecified, the target moduleId is loaded
  * into the default viewPort (the viewPort with name 'default'). The viewPorts object should have keys
  * whose property names correspond to names used by <router-view> elements. The values should be objects
  * specifying the moduleId to load into that viewPort.
  */
  viewPorts?: any;

  /**
  * When specified, this route will be included in the [[Router.navigation]] nav model. Useful for
  * dynamically generating menus or other navigation elements. When a number is specified, that value
  * will be used as a sort order.
  */
  nav?: boolean|number;

  /**
  * The URL fragment to use in nav models. If unspecified, the [[RouteConfig.route]] will be used.
  * However, if the [[RouteConfig.route]] contains dynamic segments, this property must be specified.
  */
  href?: string;

  /**
  * The document title to set when this route is active.
  */
  title?: string;

  /**
  * Arbitrary data to attach to the route. This can be used to attached custom data needed by components
  * like pipeline steps and activated modules.
  */
  settings?: any;

  /**
  * The navigation model for storing and interacting with the route's navigation settings.
  */
  navModel?: NavModel;

  [x: string]: any;
}
<|endoftext|>


<commit_before>let MessageTags = require('./messagetags');

module.exports = class IrcMessage {
    constructor(command, ...args) {
        this.tags = Object.create(null);
        this.prefix = '';
        this.nick = '';
        this.ident = '';
        this.hostname = '';
        this.command = command || '';
        this.params = args || [];
    }

    to1459() {
        let parts = [];

        let tags = MessageTags.encode(this.tags);
        if (tags) {
            parts.push('@' + tags);
        }

        if (this.prefix) {
            // TODO: If prefix is empty, build it from the nick!ident@hostname
            parts.push(':' + this.prefix);
        }

        parts.push(this.command);

        if (this.params.length > 0) {
            this.params.forEach((param, idx) => {
                if (idx === this.params.length - 1 && param.indexOf(' ') > -1) {
                    parts.push(':' + param);
                } else {
                    parts.push(param);
                }
            });
        }

        return parts.join(' ');
    }

    toJson() {
        return {
            tags: Object.assign({}, this.tags),
            source: this.prefix,
            command: this.command,
            params: this.params,
        };
    }
};
<commit_msg>Use trailing if last param starts with a semicolon<commit_after>let MessageTags = require('./messagetags');

module.exports = class IrcMessage {
    constructor(command, ...args) {
        this.tags = Object.create(null);
        this.prefix = '';
        this.nick = '';
        this.ident = '';
        this.hostname = '';
        this.command = command || '';
        this.params = args || [];
    }

    to1459() {
        let parts = [];

        let tags = MessageTags.encode(this.tags);
        if (tags) {
            parts.push('@' + tags);
        }

        if (this.prefix) {
            // TODO: If prefix is empty, build it from the nick!ident@hostname
            parts.push(':' + this.prefix);
        }

        parts.push(this.command);

        if (this.params.length > 0) {
            this.params.forEach((param, idx) => {
                if (idx === this.params.length - 1 && (param.indexOf(' ') > -1 || param.indexOf(':') === 0)) {
                    parts.push(':' + param);
                } else {
                    parts.push(param);
                }
            });
        }

        return parts.join(' ');
    }

    toJson() {
        return {
            tags: Object.assign({}, this.tags),
            source: this.prefix,
            command: this.command,
            params: this.params,
        };
    }
};
<|endoftext|>


<commit_before>/*
 * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
 * http://paperjs.org/
 *
 * Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * All rights reserved.
 */

/**
 * @name Group
 *
 * @class A Group is a collection of items. When you transform a Group, its
 * children are treated as a single unit without changing their relative
 * positions.
 *
 * @extends Item
 */
var Group = this.Group = Item.extend(/** @lends Group# */{
	_type: 'Group',
	_serializeFields: {
		children: []
	},

	// DOCS: document new Group(item, item...);
	/**
	 * Creates a new Group item and places it at the top of the active layer.
	 *
	 * @param {Item[]} [children] An array of children that will be added to the
	 * newly created group.
	 *
	 * @example {@paperscript split=true height=200}
	 * // Create a group containing two paths:
	 * var path = new Path(new Point(100, 100), new Point(100, 200));
	 * var path2 = new Path(new Point(50, 150), new Point(150, 150));
	 *
	 * // Create a group from the two paths:
	 * var group = new Group([path, path2]);
	 *
	 * // Set the stroke color of all items in the group:
	 * group.strokeColor = 'black';
	 *
	 * // Move the group to the center of the view:
	 * group.position = view.center;
	 *
	 * @example {@paperscript split=true height=320}
	 * // Click in the view to add a path to the group, which in turn is rotated
	 * // every frame:
	 * var group = new Group();
	 *
	 * function onMouseDown(event) {
	 * 	// Create a new circle shaped path at the position
	 * 	// of the mouse:
	 * 	var path = new Path.Circle(event.point, 5);
	 * 	path.fillColor = 'black';
	 *
	 * 	// Add the path to the group's children list:
	 * 	group.addChild(path);
	 * }
	 *
	 * function onFrame(event) {
	 * 	// Rotate the group by 1 degree from
	 * 	// the centerpoint of the view:
	 * 	group.rotate(1, view.center);
	 * }
	 */
	initialize: function(arg) {
		this.base();
		// Allow Group to have children and named children
		this._children = [];
		this._namedChildren = {};
		if (!this._set(arg))
			this.addChildren(Array.isArray(arg) ? arg : arguments);
	},

	_changed: function(flags) {
		// Don't use this.base() for reasons of performance.
		Item.prototype._changed.call(this, flags);
		if (flags & (/*#=*/ ChangeFlag.HIERARCHY | /*#=*/ ChangeFlag.CLIPPING)) {
			// Clear cached clip item whenever hierarchy changes
			delete this._clipItem;
		}
	},

	_getClipItem: function() {
		// Allow us to set _clipItem to null when none is found and still return
		// it as a defined value without searching again
		if (this._clipItem !== undefined)
			return this._clipItem;
		for (var i = 0, l = this._children.length; i < l; i++) {
			var child = this._children[i];
			if (child._clipMask)
				return this._clipItem = child;
		}
		// Make sure we're setting _clipItem to null so it won't be searched for
		// nex time.
		return this._clipItem = null;
	},

	/**
	 * Specifies whether the group item is to be clipped.
	 * When setting to {@code true}, the first child in the group is
	 * automatically defined as the clipping mask.
	 *
	 * @type Boolean
	 * @bean
	 */
	isClipped: function() {
		return !!this._getClipItem();
	},

	setClipped: function(clipped) {
		var child = this.getFirstChild();
		if (child)
			child.setClipMask(clipped);
	},

	draw: function(ctx, param) {
		var clipItem = this._getClipItem();
		if (clipItem) {
			param.clipping = true;
			Item.draw(clipItem, ctx, param);
			delete param.clipping;
		}
		for (var i = 0, l = this._children.length; i < l; i++) {
			var item = this._children[i];
			if (item != clipItem)
				Item.draw(item, ctx, param);
		}
	}
});
<commit_msg>Improve Group documentation.<commit_after>/*
 * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
 * http://paperjs.org/
 *
 * Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * All rights reserved.
 */

/**
 * @name Group
 *
 * @class A Group is a collection of items. When you transform a Group, its
 * children are treated as a single unit without changing their relative
 * positions.
 *
 * @extends Item
 */
var Group = this.Group = Item.extend(/** @lends Group# */{
	_type: 'Group',
	_serializeFields: {
		children: []
	},

	// DOCS: document new Group(item, item...);
	/**
	 * Creates a new Group item and places it at the top of the active layer.
	 *
	 * @name Group#initialize
	 * @param {Item[]} [children] An array of children that will be added to the
	 * newly created group.
	 *
	 * @example {@paperscript}
	 * // Create a group containing two paths:
	 * var path = new Path([100, 100], [100, 200]);
	 * var path2 = new Path([50, 150], [150, 150]);
	 *
	 * // Create a group from the two paths:
	 * var group = new Group([path, path2]);
	 *
	 * // Set the stroke color of all items in the group:
	 * group.strokeColor = 'black';
	 *
	 * // Move the group to the center of the view:
	 * group.position = view.center;
	 *
	 * @example {@paperscript height=320}
	 * // Click in the view to add a path to the group, which in turn is rotated
	 * // every frame:
	 * var group = new Group();
	 *
	 * function onMouseDown(event) {
	 * 	// Create a new circle shaped path at the position
	 * 	// of the mouse:
	 * 	var path = new Path.Circle(event.point, 5);
	 * 	path.fillColor = 'black';
	 *
	 * 	// Add the path to the group's children list:
	 * 	group.addChild(path);
	 * }
	 *
	 * function onFrame(event) {
	 * 	// Rotate the group by 1 degree from
	 * 	// the centerpoint of the view:
	 * 	group.rotate(1, view.center);
	 * }
	 */
	/**
	 * Creates a new Group item and places it at the top of the active layer.
	 *
	 * @name Group#initialize
	 * @param {Object} properties An object literal containing properties to be
	 * set on the Group.
	 *
	 * @example {@paperscript}
	 * var path = new Path([100, 100], [100, 200]);
	 * var path2 = new Path([50, 150], [150, 150]);
	 *
	 * // Create a group from the two paths:
	 * var group = new Group({
	 * 	children: [path, path2],
	 * 	// Set the stroke color of all items in the group:
	 * 	strokeColor: 'black',
	 * 	// Move the group to the center of the view:
	 * 	position: view.center
	 * });
	 */
	initialize: function(arg) {
		this.base();
		// Allow Group to have children and named children
		this._children = [];
		this._namedChildren = {};
		if (!this._set(arg))
			this.addChildren(Array.isArray(arg) ? arg : arguments);
	},

	_changed: function(flags) {
		// Don't use this.base() for reasons of performance.
		Item.prototype._changed.call(this, flags);
		if (flags & (/*#=*/ ChangeFlag.HIERARCHY | /*#=*/ ChangeFlag.CLIPPING)) {
			// Clear cached clip item whenever hierarchy changes
			delete this._clipItem;
		}
	},

	_getClipItem: function() {
		// Allow us to set _clipItem to null when none is found and still return
		// it as a defined value without searching again
		if (this._clipItem !== undefined)
			return this._clipItem;
		for (var i = 0, l = this._children.length; i < l; i++) {
			var child = this._children[i];
			if (child._clipMask)
				return this._clipItem = child;
		}
		// Make sure we're setting _clipItem to null so it won't be searched for
		// nex time.
		return this._clipItem = null;
	},

	/**
	 * Specifies whether the group item is to be clipped.
	 * When setting to {@code true}, the first child in the group is
	 * automatically defined as the clipping mask.
	 *
	 * @type Boolean
	 * @bean
	 *
	 * @example {@paperscript}
	 * var star = new Path.Star({
	 * 	center: view.center,
	 * 	points: 6,
	 * 	radius1: 20,
	 * 	radius2: 40,
	 * 	fillColor: 'red'
	 * });
	 *
	 * var circle = new Path.Circle({
	 * 	center: view.center,
	 * 	radius: 25,
	 * 	strokeColor: 'black'
	 * });
	 *
	 * // Create a group of the two items and clip it:
	 * var group = new Group(circle, star);
	 * group.clipped = true;
	 *
	 * // Lets animate the circle:
	 * function onFrame(event) {
	 * 	var offset = Math.sin(event.count / 30) * 30;
	 * 	circle.position.x = view.center.x + offset;
	 * }
	 */
	isClipped: function() {
		return !!this._getClipItem();
	},

	setClipped: function(clipped) {
		var child = this.getFirstChild();
		if (child)
			child.setClipMask(clipped);
	},

	draw: function(ctx, param) {
		var clipItem = this._getClipItem();
		if (clipItem) {
			param.clipping = true;
			Item.draw(clipItem, ctx, param);
			delete param.clipping;
		}
		for (var i = 0, l = this._children.length; i < l; i++) {
			var item = this._children[i];
			if (item != clipItem)
				Item.draw(item, ctx, param);
		}
	}
});
<|endoftext|>


<commit_before>define(['jquery', 'bibtex_js', 'FileSaver', 'codemirror', 'app/util', 'data/generated/bib', 'data/generated/available_pdf', 'data/generated/available_img', 'data/search_stopwords', 'data/tag_categories', 'data/authorized_tags'],
    function ($, bibtexJS, fileSaver, CodeMirror, util, generatedBib, availaiblePdf, availableImg, stopwords, tagCategories, authorizedTags) {

        var entries = readBibtex();
        if (!entries) {
            entries = generatedBib.entries;
        }

        return {
            entries: entries,
            availablePdf: availaiblePdf.availablePdf,
            availableImg: availableImg.availableImg,
            stopwords: stopwords.stopwords,
            tagCategories: tagCategories.tagCategories,
            authorizedTags: authorizedTags.authorizedTags,
            entryDivs: {},
            warnings: warnings.computeAllWarnings(entries),
            nVisibleEntries: 20,

            downloadBibtex: function () {
                var blob = new Blob([this.createAllBibtex()]);
                window.saveAs(blob, 'reference.bib');
            },

            createBibtex: function (id, entry) {
                var bibtex = '@' + entry['type'] + '{' + id;
                for (var fieldName in entry) {
                    if (fieldName == 'type' || util.isFieldForbidden(fieldName)) {
                        continue;
                    }
                    if (entry.hasOwnProperty(fieldName)) {
                        bibtex += ",\n  " + fieldName + " = {" + entry[fieldName] + "}";
                    }
                }
                if (typeof mandatoryFields != 'undefined' && mandatoryFields) {
                    for (var i in mandatoryFields[entry["type"]]) {
                        if (!entry.hasOwnProperty(mandatoryFields[entry["type"]][i])) {
                            bibtex += ",\n  " + mandatoryFields[entry["type"]][i] + " = {}";
                        }
                    }
                }
                return bibtex + "\n}";
            },

            createCitation: function (id) {
                var bib = this;
                var citation = '';
                if (bib.parsedEntries[id]['author']) {
                    $.each(bib.parsedEntries[id]['author'], function (i, author) {
                        var authorSplit = author.split(', ');
                        if (authorSplit.length == 2) {
                            author = authorSplit[0] + ', ' + authorSplit[1].replace(/[^a-z -]/gi, '').replace(/\B\w*/g, '.').replace(/([A-Z])($|([ -]))/g, '$1.$3');
                        }
                        if (i == bib.parsedEntries[id]['author'].length - 1 && i > 0) {
                            citation += 'and ';
                        }
                        citation += util.latexToHtml(author);
                        if (bib.parsedEntries[id]['author'].length > 2 || i > 0) {
                            citation += ', ';
                        } else {
                            citation += ' ';
                        }
                    });
                }
                var year = bib.entries[id]['year'];
                if (year) {
                    citation += year + '. ';
                }
                if (title = bib.entries[id]['title']) {
                    citation += util.latexToHtml(title) + '. ';
                }
                var journal = bib.entries[id]['journal'];
                var pages = bib.entries[id]['pages'];
                if (journal) {
                    citation += 'In ' + util.latexToHtml(journal) + '';
                    var volume = bib.entries[id]['volume'];
                    if (volume) {
                        citation += ' (Vol. ' + util.latexToHtml(volume);
                        var number = bib.entries[id]['number'];
                        if (number) {
                            citation += ', No. ' + util.latexToHtml(number);
                        }
                        if (pages) {
                            citation += ', pp. ' + util.latexToHtml(pages);
                        }
                        citation += ')';
                    }
                    citation += '. ';
                }
                var booktitle = bib.entries[id]['booktitle'];
                if (booktitle) {
                    citation += 'In ' + util.latexToHtml(booktitle) + '';
                    if (pages) {
                        citation += ' (pp. ' + util.latexToHtml(pages) + ')';
                    }
                    citation += '. ';
                }
                var doi = bib.entries[id]['doi'];
                var url = bib.entries[id]['url'];
                if (doi) {
                    citation += 'DOI: ' + doi + '. ';
                } else if (url) {
                    citation += 'URL: ' + url + '. ';
                }
                return citation.trim();
            },

            createAllBibtex: function () {
                var bib = this;
                var bibtexString = '';
                $.each(bib.filteredEntries, function (id, entry) {
                    var currentBibtex = "";
                    if (bib.entryDivs[id]) {
                        bib.entryDivs[id].find(".CodeMirror-code").children().each(function () {
                            currentBibtex += $(this).text() + "\n";
                        });
                    }
                    if (!currentBibtex) {
                        currentBibtex = bib.createBibtex(id, entry);
                    }
                    bibtexString += currentBibtex;
                    bibtexString += "\n\n";
                });
                return bibtexString;
            },

            saveBibToLocalStorage: function () {
                if (editable) {
                    localStorage.bibtexString = this.createAllBibtex();
                }
            },

            addEntries: function () {
                var bib = this;
                var addEntriesDiv = $('
', {
                    id: 'add_entries',
                    title: 'Add entries',
                    text: 'Paste one or more BibTeX entries:'
                }).appendTo($('body'));
                var bibtexEditor = CodeMirror(addEntriesDiv.get(0), {
                    lineWrapping: true
                });
                var bibtexStatusDiv = $('
', {
                    class: 'bibtex_status'
                }).appendTo(addEntriesDiv);
                bibtexEditor.on('change', function (bibtexEditor) {
                    bibtexStatusDiv.empty();
                    var addButton = $('.add_entry_button');
                    var addButtonTextDiv = addButton.find('.ui-button-text');
                    try {
                        var bibtexText = bibtexEditor.getValue();
                        var bibtexEntries = bib.parse(bibtexText);
                        var nEntries = Object.keys(bibtexEntries).length;
                        if (nEntries > 0) {
                            addButtonTextDiv.text(
                                'add (' + nEntries + (nEntries > 1 ? ' entries)' : ' entry)')
                            );
                            addButton.button('enable');
                        } else {
                            addButtonTextDiv.text('add');
                            addButton.button('disable');
                        }
                    }
                    catch (err) {
                        $('
', {
                            text: err,
                            class: 'error'
                        }).appendTo(bibtexStatusDiv);
                        addButton.button('disable');
                    }
                });
                addEntriesDiv.dialog({
                    minWidth: 832,
                    modal: true,
                    buttons: {
                        'Add': {
                            text: 'add',
                            class: 'add_entry_button',
                            disabled: true,
                            click: function () {
                                var bibtexText = bibtexEditor.getValue();
                                addEntriesDiv.dialog('close');
                                if (bibtexText != null) {
                                    var bibtexEntries = bib.parse(bibtexText);
                                    for (var entryKey in bibtexEntries) {
                                        var bibtexEntry = bibtexEntries[entryKey];
                                        if (bib.entries[entryKey]) {
                                            alert('Entry with ID "' + entryKey + '" already exists and cannot be added to the database.');
                                        } else {
                                            bib.entries[entryKey] = {};
                                            for (var key in bibtexEntry) {
                                                var keyLower = key.toLowerCase();
                                                bib.entries[entryKey][keyLower] = bibtexEntry[key];
                                            }
                                            var mandatoryFields = ['author', 'year', 'title', 'abstract', 'doi', 'series'];
                                            $.each(mandatoryFields, function (i, field) {
                                                if (!bib.entries[entryKey][field]) {
                                                    bib.entries[entryKey][field] = '';
                                                }
                                            });
                                            bib.warnings[entryKey] = warnings.computeWarnings(bib.entries[entryKey]);
                                        }
                                    }
                                }
                                if (Object.keys(bibtexEntries).length == 1) {
                                    window.toggleSelector('search', Object.keys(bibtexEntries)[0]);
                                }
                                update();
                            }
                        },
                        cancel: function () {
                            $(this).dialog("close");
                        }
                    }
                });
                bibtexEditor.focus();
            },

            parse: function (bibtexText) {
                var bibParser = new BibtexParser();
                bibParser.setInput(bibtexText);
                bibParser.bibtex();
                return bibParser.getEntries();
            }
        };

        function readBibtex() {
            var bibParser = new BibtexParser();
            var loadFromLocalStorage = util.getUrlParameter('loadFromLocalStorage') === 'true';
            if (editable && loadFromLocalStorage && localStorage.bibtexString) {
                try {
                    bibParser.setInput(localStorage.bibtexString);
                    bibParser.bibtex();
                    return bibParser.getEntries();
                } catch (err) {
                    console.error(err);
                    console.log(localStorage.bibtexString);
                    alert('Could not load bibliography from local storage, loaded default instead (see console for details and locally stored bibliography): \n\n' + err.substring(0, 200));
                }
                return null;
            }
        }

    });
<commit_msg>reduced the number of mandatory bib fields<commit_after>define(['jquery', 'bibtex_js', 'FileSaver', 'codemirror', 'app/util', 'data/generated/bib', 'data/generated/available_pdf', 'data/generated/available_img', 'data/search_stopwords', 'data/tag_categories', 'data/authorized_tags'],
    function ($, bibtexJS, fileSaver, CodeMirror, util, generatedBib, availaiblePdf, availableImg, stopwords, tagCategories, authorizedTags) {

        var entries = readBibtex();
        if (!entries) {
            entries = generatedBib.entries;
        }

        return {
            entries: entries,
            availablePdf: availaiblePdf.availablePdf,
            availableImg: availableImg.availableImg,
            stopwords: stopwords.stopwords,
            tagCategories: tagCategories.tagCategories,
            authorizedTags: authorizedTags.authorizedTags,
            entryDivs: {},
            warnings: warnings.computeAllWarnings(entries),
            nVisibleEntries: 20,

            downloadBibtex: function () {
                var blob = new Blob([this.createAllBibtex()]);
                window.saveAs(blob, 'reference.bib');
            },

            createBibtex: function (id, entry) {
                var bibtex = '@' + entry['type'] + '{' + id;
                for (var fieldName in entry) {
                    if (fieldName == 'type' || util.isFieldForbidden(fieldName)) {
                        continue;
                    }
                    if (entry.hasOwnProperty(fieldName)) {
                        bibtex += ",\n  " + fieldName + " = {" + entry[fieldName] + "}";
                    }
                }
                if (typeof mandatoryFields != 'undefined' && mandatoryFields) {
                    for (var i in mandatoryFields[entry["type"]]) {
                        if (!entry.hasOwnProperty(mandatoryFields[entry["type"]][i])) {
                            bibtex += ",\n  " + mandatoryFields[entry["type"]][i] + " = {}";
                        }
                    }
                }
                return bibtex + "\n}";
            },

            createCitation: function (id) {
                var bib = this;
                var citation = '';
                if (bib.parsedEntries[id]['author']) {
                    $.each(bib.parsedEntries[id]['author'], function (i, author) {
                        var authorSplit = author.split(', ');
                        if (authorSplit.length == 2) {
                            author = authorSplit[0] + ', ' + authorSplit[1].replace(/[^a-z -]/gi, '').replace(/\B\w*/g, '.').replace(/([A-Z])($|([ -]))/g, '$1.$3');
                        }
                        if (i == bib.parsedEntries[id]['author'].length - 1 && i > 0) {
                            citation += 'and ';
                        }
                        citation += util.latexToHtml(author);
                        if (bib.parsedEntries[id]['author'].length > 2 || i > 0) {
                            citation += ', ';
                        } else {
                            citation += ' ';
                        }
                    });
                }
                var year = bib.entries[id]['year'];
                if (year) {
                    citation += year + '. ';
                }
                if (title = bib.entries[id]['title']) {
                    citation += util.latexToHtml(title) + '. ';
                }
                var journal = bib.entries[id]['journal'];
                var pages = bib.entries[id]['pages'];
                if (journal) {
                    citation += 'In ' + util.latexToHtml(journal) + '';
                    var volume = bib.entries[id]['volume'];
                    if (volume) {
                        citation += ' (Vol. ' + util.latexToHtml(volume);
                        var number = bib.entries[id]['number'];
                        if (number) {
                            citation += ', No. ' + util.latexToHtml(number);
                        }
                        if (pages) {
                            citation += ', pp. ' + util.latexToHtml(pages);
                        }
                        citation += ')';
                    }
                    citation += '. ';
                }
                var booktitle = bib.entries[id]['booktitle'];
                if (booktitle) {
                    citation += 'In ' + util.latexToHtml(booktitle) + '';
                    if (pages) {
                        citation += ' (pp. ' + util.latexToHtml(pages) + ')';
                    }
                    citation += '. ';
                }
                var doi = bib.entries[id]['doi'];
                var url = bib.entries[id]['url'];
                if (doi) {
                    citation += 'DOI: ' + doi + '. ';
                } else if (url) {
                    citation += 'URL: ' + url + '. ';
                }
                return citation.trim();
            },

            createAllBibtex: function () {
                var bib = this;
                var bibtexString = '';
                $.each(bib.filteredEntries, function (id, entry) {
                    var currentBibtex = "";
                    if (bib.entryDivs[id]) {
                        bib.entryDivs[id].find(".CodeMirror-code").children().each(function () {
                            currentBibtex += $(this).text() + "\n";
                        });
                    }
                    if (!currentBibtex) {
                        currentBibtex = bib.createBibtex(id, entry);
                    }
                    bibtexString += currentBibtex;
                    bibtexString += "\n\n";
                });
                return bibtexString;
            },

            saveBibToLocalStorage: function () {
                if (editable) {
                    localStorage.bibtexString = this.createAllBibtex();
                }
            },

            addEntries: function () {
                var bib = this;
                var addEntriesDiv = $('
', {
                    id: 'add_entries',
                    title: 'Add entries',
                    text: 'Paste one or more BibTeX entries:'
                }).appendTo($('body'));
                var bibtexEditor = CodeMirror(addEntriesDiv.get(0), {
                    lineWrapping: true
                });
                var bibtexStatusDiv = $('
', {
                    class: 'bibtex_status'
                }).appendTo(addEntriesDiv);
                bibtexEditor.on('change', function (bibtexEditor) {
                    bibtexStatusDiv.empty();
                    var addButton = $('.add_entry_button');
                    var addButtonTextDiv = addButton.find('.ui-button-text');
                    try {
                        var bibtexText = bibtexEditor.getValue();
                        var bibtexEntries = bib.parse(bibtexText);
                        var nEntries = Object.keys(bibtexEntries).length;
                        if (nEntries > 0) {
                            addButtonTextDiv.text(
                                'add (' + nEntries + (nEntries > 1 ? ' entries)' : ' entry)')
                            );
                            addButton.button('enable');
                        } else {
                            addButtonTextDiv.text('add');
                            addButton.button('disable');
                        }
                    }
                    catch (err) {
                        $('
', {
                            text: err,
                            class: 'error'
                        }).appendTo(bibtexStatusDiv);
                        addButton.button('disable');
                    }
                });
                addEntriesDiv.dialog({
                    minWidth: 832,
                    modal: true,
                    buttons: {
                        'Add': {
                            text: 'add',
                            class: 'add_entry_button',
                            disabled: true,
                            click: function () {
                                var bibtexText = bibtexEditor.getValue();
                                addEntriesDiv.dialog('close');
                                if (bibtexText != null) {
                                    var bibtexEntries = bib.parse(bibtexText);
                                    for (var entryKey in bibtexEntries) {
                                        var bibtexEntry = bibtexEntries[entryKey];
                                        if (bib.entries[entryKey]) {
                                            alert('Entry with ID "' + entryKey + '" already exists and cannot be added to the database.');
                                        } else {
                                            bib.entries[entryKey] = {};
                                            for (var key in bibtexEntry) {
                                                var keyLower = key.toLowerCase();
                                                bib.entries[entryKey][keyLower] = bibtexEntry[key];
                                            }
                                            var mandatoryFields = ['author', 'year', 'title'];
                                            $.each(mandatoryFields, function (i, field) {
                                                if (!bib.entries[entryKey][field]) {
                                                    bib.entries[entryKey][field] = '';
                                                }
                                            });
                                            bib.warnings[entryKey] = warnings.computeWarnings(bib.entries[entryKey]);
                                        }
                                    }
                                }
                                if (Object.keys(bibtexEntries).length == 1) {
                                    window.toggleSelector('search', Object.keys(bibtexEntries)[0]);
                                }
                                update();
                            }
                        },
                        cancel: function () {
                            $(this).dialog("close");
                        }
                    }
                });
                bibtexEditor.focus();
            },

            parse: function (bibtexText) {
                var bibParser = new BibtexParser();
                bibParser.setInput(bibtexText);
                bibParser.bibtex();
                return bibParser.getEntries();
            }
        };

        function readBibtex() {
            var bibParser = new BibtexParser();
            var loadFromLocalStorage = util.getUrlParameter('loadFromLocalStorage') === 'true';
            if (editable && loadFromLocalStorage && localStorage.bibtexString) {
                try {
                    bibParser.setInput(localStorage.bibtexString);
                    bibParser.bibtex();
                    return bibParser.getEntries();
                } catch (err) {
                    console.error(err);
                    console.log(localStorage.bibtexString);
                    alert('Could not load bibliography from local storage, loaded default instead (see console for details and locally stored bibliography): \n\n' + err.substring(0, 200));
                }
                return null;
            }
        }

    });
<|endoftext|>