Programing

Redex의 대기열 동작

c10106 2022. 3. 22. 08:47
반응형

Redex의 대기열 동작

현재 Redex Actions를 연속적으로 실행해야 하는 상황이 있다.나는 여러 가지 미들웨어, 그러한 환원 약속들을 살펴봤는데, 그 연이은 행동들이 (더 나은 용어가 없어서) 촉발되는 근본의 지점에 어떤 것이 있는지 알면 괜찮은 것 같다.

본질적으로, 나는 어느 시점에나 추가될 수 있는 일련의 행동들을 유지하고 싶다.각 개체는 그 상태에 이 대기열의 인스턴스를 가지고 있으며 그에 따라 종속적인 동작은 열거, 처리 및 디큐레이션될 수 있다.나는 구현이 있지만, 그렇게 함으로써 나는 내 액션 크리에이터의 상태에 접근하고 있다. 그것은 반패턴적인 느낌이다.

사용 사례와 구현에 대한 컨텍스트를 제공하도록 노력하겠다.

Use Case

일부 목록을 만들어 서버에서 계속 유지하려고 한다고 가정해 보십시오.목록 작성 시 서버는 해당 목록에 대한 ID로 응답하며, 이 ID는 목록과 관련된 후속 API 엔드 포인트에서 사용된다.

http://my.api.com/v1.0/lists/           // POST returns some id
http://my.api.com/v1.0/lists/<id>/items // API end points include id

고객이 UX를 향상시키기 위해 이러한 API 포인트에 대해 낙관적인 업데이트를 수행하기를 원한다고 상상해 보십시오. 아무도 스핀너를 보는 것을 좋아하지 않는다.따라서 목록을 만들면 새 목록이 즉시 표시되고 항목 추가 시 옵션:

+-------------+----------+
|  List Name  | Actions  |
+-------------+----------+
| My New List | Add Item |
+-------------+----------+

초기 생성 통화의 응답으로 인해 다시 돌아오기 전에 누군가 항목을 추가하려고 시도한다고 가정해 보십시오.아이템 API는 아이디에 따라 달라지기 때문에 그 데이터를 얻기 전에는 호출할 수 없다는 것을 알고 있다.그러나, 우리는 새로운 아이템을 최적으로 보여주고 API 항목에 대한 통화를 수신하여, 일단 생성 콜이 완료되면 그것이 트리거되도록 하는 것을 원할 수 있다.

잠재적인 해결책

현재 이 문제를 해결하기 위해 사용하고 있는 방법은 각 목록에 액션 대기열, 즉 연속적으로 트리거될 Redex 액션 목록을 제공하는 것이다.

목록 작성을 위한 환원기 기능은 다음과 같을 수 있다.

case ADD_LIST:
  return {
    id: undefined, // To be filled on server response
    name: action.payload.name,
    actionQueue: []
  }

그리고 나서, 액션 크리에이터에서, 우리는 액션을 직접적으로 촉발시키는 대신에, 액션을 촉진할 것이다:

export const createListItem = (name) => {
    return (dispatch) => {
        dispatch(addList(name));  // Optimistic action
        dispatch(enqueueListAction(name, backendCreateListAction(name));
    }
}

단순성을 위해 backendCreateListAction 함수가 fetch API를 호출한다고 가정하고, 이 기능은 성공/실패에 대한 목록에서 삭제하기 위해 메시지를 발송한다.

문제

여기서 내가 걱정하는 것은 enqueListAction 방법의 실행이다.여기가 내가 주(州)에 접속해서 대기열의 진보를 통제하는 곳이야이와 비슷한 것으로 보인다(이 이름에 일치하는 것을 무시함 - 실제로는 clientId를 사용하지만, 나는 그 예를 단순하게 유지하려고 노력한다).

const enqueueListAction = (name, asyncAction) => {
    return (dispatch, getState) => {
        const state = getState();

        dispatch(enqueue(name, asyncAction));{

        const thisList = state.lists.find((l) => {
            return l.name == name;
        });

        // If there's nothing in the queue then process immediately
        if (thisList.actionQueue.length === 0) {
            asyncAction(dispatch);
        } 
    }
}

여기서 enqueue 메서드가 actionQueue 목록에 비동기 액션을 삽입하는 일반 액션을 반환한다고 가정한다.

모든 것이 곡식에 약간 어긋난다고 느끼지만, 다른 방법이 있을지는 잘 모르겠다.또한 비동기식으로 발송해야 하므로 발송 방법을 그들에게 전달해야 한다.

리스트에서 삭제하는 방법에는 유사한 코드가 있으며, 이는 다음 조치가 존재할 경우 트리거한다.

const dequeueListAction = (name) => {
    return (dispatch, getState) => {
        dispatch(dequeue(name));

        const state = getState();
        const thisList = state.lists.find((l) => {
            return l.name === name;
        });

        // Process next action if exists.
        if (thisList.actionQueue.length > 0) {
            thisList.actionQueue[0].asyncAction(dispatch);
    }
}

일반적으로 말해서 나는 이것을 가지고 살 수 있지만, 그것이 반패턴적인 것이고, 이것을 하는 더 간결하고 관용적인 방법이 Redex에 있을지도 모른다는 걱정이 든다.

어떤 도움이라도 감사하다.

나는 네가 찾고 있는 것을 위한 완벽한 도구를 가지고 있다.Redex에 대한 제어력이 많이 필요하고(특히 비동기식) 순차적으로 발생하기 위해 Redx 작업이 필요한 경우 Redex Sagas보다 더 좋은 툴은 없다.es6 발전기 위에 구축되어 어떤 점에서 코드를 일시 정지시킬 수 있기 때문에 많은 제어력을 제공한다.

당신이 묘사하는 액션 대기열은 소위 사극이라고 불리는 것이다.이제 이 sagas는 환원제로 작동하도록 만들어졌기 때문에, 당신의 구성품을 파견함으로써 작동되도록 유발될 수 있다.

Sagas는 발전기를 사용하기 때문에, 당신은 또한 당신의 디스패치가 특정한 순서에 따라 발생하며 특정한 조건에서만 발생한다는 것을 확신할 수 있다.다음은 그들의 설명서에서 나온 예시인데, 내가 의미하는 바를 설명하기 위해 너에게 안내해 줄게.

function* loginFlow() {
  while (true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    const token = yield call(authorize, user, password)
    if (token) {
      yield call(Api.storeItem, {token})
      yield take('LOGOUT')
      yield call(Api.clearItem, 'token')
    }
  }
}

좋아, 처음에는 좀 혼란스러워 보이지만, 이 이야기는 로그인 순서가 일어나야 하는 정확한 순서를 정의한다.발전기의 특성상 무한순환이 허용된다.당신의 코드가 항복하면 그것은 그 선에서 멈추고 기다릴 것이다.그것은 당신이 그것을 지시할 때까지 다음 줄까지 계속되지 않을 것이다.그러니 어디라고 써있는지 봐.yield take('LOGIN_REQUEST') 'LOGIN_REQEST까지 이 그 까지 갈 'LOGIN_REQEST'를 발송할 때까지 이 시점에서 사연은 양보하거나 기다릴 것이며, 그 후에 사연은 승인 방법을 호출하고, 다음 항복까지 갈 것이다.다음 방법은 비동기식이다.yield call(Api.storeItem, {token})그 코드가 해결될 때까지 다음 라인으로 가지 않을 겁니다

자, 여기서 마법이 일어난다.그 이야기는 다시 에서 멈출 것이다.yield take('LOGOUT')응용 프로그램에서 LOGOUT를 발송할 때까지.LOGIN_REQUEST를 LOGOUT 전에 다시 발송해야 할 경우 로그인 프로세스가 호출되지 않기 때문에 매우 중요하다.이제 LOGOUT를 발송하면 첫 번째 수율까지 다시 루프되고 애플리케이션이 다시 LOGIN_REQEST를 발송할 때까지 기다린다.

Redex Sagas는 단연코 Redex와 함께 사용할 수 있는 내가 가장 좋아하는 도구 중 하나이다.그것은 당신에게 당신의 어플리케이션을 통제할 수 있게 해주며, 당신의 코드를 읽는 모든 사람들은 이제 모든 것이 한 번에 한 줄씩 읽기 때문에 당신에게 감사할 것이다.

이것을 보십시오: https://github.com/gaearon/redux-thunk

아이디만 감량기를 통과하면 안 된다.작업 작성자(thunk)에서 목록 ID를 먼저 가져온 다음() 두 번째 호출을 수행하여 항목을 목록에 추가하십시오.이후 추가가 성공했는지 여부에 따라 다른 액션을 파견할 수 있다.

이 작업을 수행하는 동안 여러 작업을 전송하여 서버 상호 작용이 시작 및 완료된 시기를 보고할 수 있다.이렇게 하면 작업이 무겁고 시간이 걸릴 수 있으므로 메시지나 스피너를 보여줄 수 있다.

보다 심도 있는 분석은 http://redux.js.org/docs/advanced/AsyncActions.html에서 확인할 수 있다.

Dan Abramov의 모든 공로

나는 너와 비슷한 문제에 직면하고 있었다.낙관적인 조치가 생성되는 것과 동일한 순차적 순서로 원격 서버에 커밋되거나 최종적으로 커밋(네트워크 문제의 경우)되도록 보장하기 위한 대기열이 필요했고, 가능하지 않으면 롤백했다.Redex만 가지고는 부족한 점이 있다는 걸 알아냈어. 왜냐하면 기본적으로 난 그것이 이것을 위해 고안된 것이 아니라고 믿기 때문이지. 그리고 약속만으로 그것을 하는 것은 정말 이해하기 어려운 문제가 될 수 있어. 대기열 상태를 어떻게든 관리해야 한다는 사실 말고도 말이야.IMHO.

나는 @pcriulan이 Redex-saga를 사용하자는 제안이 좋은 제안이었다고 생각한다.첫눈에, Redex-saga는 당신이 채널에 도달할 때까지 당신을 도울 수 있는 어떤 것도 제공하지 않는다.이는 JS 생성기 덕분에 CSP(예: Go 또는 Clojure의 비동기 참조)와 같은 다른 언어의 동시성을 처리할 수 있는 문을 열어준다.왜 CSP가 아닌 사가 패턴의 이름을 따서 지었는지에 대한 의문까지 있다 하하...어쨌든

그래서 여기 사연이 당신의 큐를 어떻게 도울 수 있는지 알아보자.

export default function* watchRequests() {
  while (true) {
    // 1- Create a channel for request actions
    const requestChan = yield actionChannel('ASYNC_ACTION');
    let resetChannel = false;

    while (!resetChannel) {
      // 2- take from the channel
      const action = yield take(requestChan);
      // 3- Note that we're using a blocking call
      resetChannel = yield call(handleRequest, action);
    }
  }
}

function* handleRequest({ asyncAction, payload }) {
  while (true) {
    try {
      // Perform action
      yield call(asyncAction, payload);
      return false;
    } catch(e) {

      if(e instanceof ConflictError) {
        // Could be a rollback or syncing again with server?
        yield put({ type: 'ROLLBACK', payload });
        // Store is out of consistency so
        // don't let waiting actions come through
        return true;
      } else if(e instanceof ConnectionError) {
        // try again
        yield call(delay, 2000);
      }

    }
  }
}

따라서 여기서 흥미로운 부분은 채널이 들어오는 동작에 대해 계속 "듣고" 있는 버퍼(대기열) 역할을 하지만 현재의 동작으로 끝날 때까지 미래의 동작을 진행하지 않는 방법이다.코드를 더 잘 파악하려면 그들의 문서를 검토해야 할지도 모르지만, 나는 그럴 가치가 있다고 생각한다.리셋 채널 파트가 필요에 따라 작동하거나 작동하지 않을 수 있음:생각:

도움이 되길 바래!

이것이 내가 이 문제에 대처하는 방법이다.

각 로컬 목록에 고유 식별자가 있는지 확인하십시오.난 여기서 백엔드 아이디에 대해 말하는 게 아니야.이름이 목록을 식별하기에 충분하지 않을 수도 있다.아직 유지되지 않은 "최적" 목록은 고유하게 식별할 수 있어야 하며, 사용자는 에지 케이스라도 동일한 이름의 목록을 2개 만들려고 할 수 있다.

목록 작성 시 캐시에 백엔드 ID 약속 추가

CreatedListIdPromiseCache[localListId] = createBackendList({...}).then(list => list.id);

항목 추가 시 Redex 스토어에서 백엔드 ID를 가져오십시오.존재하지 않는 경우 다음에서 가져오도록 하십시오.CreatedListIdCacheCreatedListIdCache가 약속을 반환하므로 반환된 ID는 비동기여야 함.

const getListIdPromise = (localListId,state) => {
  // Get id from already created list
  if ( state.lists[localListId] ) {
    return Promise.resolve(state.lists[localListId].id)
  }
  // Get id from pending list creations
  else if ( CreatedListIdPromiseCache[localListId] ) {
    return CreatedListIdPromiseCache[localListId];
  }
  // Unexpected error
  else {
    return Promise.reject(new Error("Unable to find backend list id for list with local id = " + localListId));
  }
}

이 방법을 사용하여 다음을 수행하십시오.addItem, 백엔드 ID를 사용할 수 있을 때까지 addItem이 자동으로 지연됨

// Create item, but do not attempt creation until we are sure to get a backend id
const backendListItemPromise = getListIdPromise(localListId,reduxState).then(backendListId => {
  return createBackendListItem(backendListId, itemData);
})

// Provide user optimistic feedback even if the item is not yet added to the list
dispatch(addListItemOptimistic());
backendListItemPromise.then(
  backendListItem => dispatch(addListItemCommit()),
  error => dispatch(addListItemRollback())
);

CreatedListIdPromiseCache를 치료하고 싶을 수도 있지만, 메모리 사용 요구 사항이 매우 엄격한 경우가 아니면 대부분의 앱에서는 그다지 중요하지 않을 수 있다.


또 다른 옵션은 백엔드 ID가 프런트엔드에서 UUID와 같은 것으로 계산된다는 것이다.당신의 백엔드는 이 아이디의 독특함을 확인하기만 하면 된다.따라서 백엔드가 아직 회신하지 않았더라도, 당신은 항상 낙관적으로 작성된 모든 목록에 대해 유효한 백엔드 ID를 가질 수 있다.

당신은 대기행위에 대처할 필요가 없다.그것은 데이터 흐름을 숨기고 당신의 앱을 디버깅하는 것을 더 지루하게 만들 것이다.

목록이나 항목을 만들 때 임시 ID를 사용하고, 상점에서 실제 ID를 받을 때 해당 ID를 업데이트하십시오.

이런 거? (시험은 하지 않고 아이디를 얻는다) :

EDIT : 리스트가 저장되면 자동으로 항목을 저장해야 한다는 것을 처음에는 이해하지 못했다.나는 편집했다.createList액션 창조자

/* REDUCERS & ACTIONS */

// this "thunk" action creator is responsible for :
//   - creating the temporary list item in the store with some 
//     generated unique id
//   - dispatching the action to tell the store that a temporary list
//     has been created (optimistic update)
//   - triggering a POST request to save the list in the database
//   - dispatching an action to tell the store the list is correctly
//     saved
//   - triggering a POST request for saving items related to the old
//     list id and triggering the correspondant receiveCreatedItem
//     action
const createList = (name) => {

  const tempList = {
    id: uniqueId(),
    name
  }

  return (dispatch, getState) => {
    dispatch(tempListCreated(tempList))
    FakeListAPI
      .post(tempList)
      .then(list => {
        dispatch(receiveCreatedList(tempList.id, list))

        // when the list is saved we can now safely
        // save the related items since the API
        // certainly need a real list ID to correctly
        // save an item
        const itemsToSave = getState().items.filter(item => item.listId === tempList.id)
        for (let tempItem of itemsToSave) {
          FakeListItemAPI
            .post(tempItem)
            .then(item => dispatch(receiveCreatedItem(tempItem.id, item)))
        }
      )
  }

}

const tempListCreated = (list) => ({
  type: 'TEMP_LIST_CREATED',
  payload: {
    list
  }
})

const receiveCreatedList = (oldId, list) => ({
  type: 'RECEIVE_CREATED_LIST',
  payload: {
    list
  },
  meta: {
    oldId
  }
})


const createItem = (name, listId) => {

  const tempItem = {
    id: uniqueId(),
    name,
    listId
  }

  return (dispatch) => {
    dispatch(tempItemCreated(tempItem))
  }

}

const tempItemCreated = (item) => ({
  type: 'TEMP_ITEM_CREATED',
  payload: {
    item
  }
})

const receiveCreatedItem = (oldId, item) => ({
  type: 'RECEIVE_CREATED_ITEM',
  payload: {
    item
  },
  meta: {
    oldId
  }
})

/* given this state shape :
state = {
  lists: {
    ids: [ 'list1ID', 'list2ID' ],
    byId: {
      'list1ID': {
        id: 'list1ID',
        name: 'list1'
      },
      'list2ID': {
        id: 'list2ID',
        name: 'list2'
      },
    }
    ...
  },
  items: {
    ids: [ 'item1ID','item2ID' ],
    byId: {
      'item1ID': {
        id: 'item1ID',
        name: 'item1',
        listID: 'list1ID'
      },
      'item2ID': {
        id: 'item2ID',
        name: 'item2',
        listID: 'list2ID'
      }
    }
  }
}
*/

// Here i'm using a immediately invoked function just 
// to isolate ids and byId variable to avoid duplicate
// declaration issue since we need them for both
// lists and items reducers
const lists = (() => {
  const ids = (ids = [], action = {}) => ({
    switch (action.type) {
      // when receiving the temporary list
      // we need to add the temporary id 
      // in the ids list
      case 'TEMP_LIST_CREATED':
        return [...ids, action.payload.list.id]

      // when receiving the real list
      // we need to remove the old temporary id
      // and add the real id instead
      case 'RECEIVE_CREATED_LIST':
        return ids
          .filter(id => id !== action.meta.oldId)
          .concat([action.payload.list.id])
      default:
        return ids
    }
  })

  const byId = (byId = {}, action = {}) => ({
    switch (action.type) {
      // same as above, when the the temp list
      // gets created we store it indexed by
      // its temp id
      case 'TEMP_LIST_CREATED':
        return {
          ...byId,
          [action.payload.list.id]: action.payload.list
        }

      // when we receive the real list we first
      // need to remove the old one before
      // adding the real list
      case 'RECEIVE_CREATED_LIST': {
        const {
          [action.meta.oldId]: oldList,
          ...otherLists
        } = byId
        return {
          ...otherLists,
          [action.payload.list.id]: action.payload.list
        }
      }

    }
  })

  return combineReducers({
    ids,
    byId
  })
})()

const items = (() => {
  const ids = (ids = [], action = {}) => ({
    switch (action.type) {
      case 'TEMP_ITEM_CREATED':
        return [...ids, action.payload.item.id]
      case 'RECEIVE_CREATED_ITEM':
        return ids
          .filter(id => id !== action.meta.oldId)
          .concat([action.payload.item.id])
      default:
        return ids
    }
  })

  const byId = (byId = {}, action = {}) => ({
    switch (action.type) {
      case 'TEMP_ITEM_CREATED':
        return {
          ...byId,
          [action.payload.item.id]: action.payload.item
        }
      case 'RECEIVE_CREATED_ITEM': {
        const {
          [action.meta.oldId]: oldList,
          ...otherItems
        } = byId
        return {
          ...otherItems,
          [action.payload.item.id]: action.payload.item
        }
      }

      // when we receive a real list
      // we need to reappropriate all
      // the items that are referring to
      // the old listId to the new one
      case 'RECEIVE_CREATED_LIST': {
        const oldListId = action.meta.oldId
        const newListId = action.payload.list.id
        const _byId = {}
        for (let id of Object.keys(byId)) {
          let item = byId[id]
          _byId[id] = {
            ...item,
            listId: item.listId === oldListId ? newListId : item.listId
          }
        }
        return _byId
      }

    }
  })

  return combineReducers({
    ids,
    byId
  })
})()

const reducer = combineReducers({
  lists,
  items
})

/* REDUCERS & ACTIONS */

참조URL: https://stackoverflow.com/questions/38742335/queuing-actions-in-redux

반응형