Можно ли преобразовать итератор саги в обычное обещание?

Я создаю уровень абстракции для веб-расширения keepassxc. Он использует каналы Redux-Saga, чтобы синхронизировать обмен сообщениями в Chrome. Он работает (не)на удивление хорошо. Однако я хочу полностью абстрагировать редукс-сагу, чтобы она выглядела как обычная функция, возвращающая Promise.

тл;др

KeePassXC-browser будет расширением для браузера, которое позволит получать пароли, хранящиеся в приложении KeePassXC, из браузера. Возможны два протокола связи: HTTP и NativeClient. Поэтому я решил использовать интерфейс машинописного текста, и в зависимости от протокола связи будет два класса, реализующих этот интерфейс.

Интерфейс:

interface Keepass {
  getDatabaseHash(): Promise<string>;
  getCredentials(origin: string, formUrl: string): Promise<KeepassCredentials[]>;
  associate(): Promise<KeepassAssociation>;
  isAssociated(dbHash: string): Promise<boolean>;
}

Первая реализация, представляющая протокол связи HTTP использует fetch API, который уже основан на Promise, поэтому реализация проста и на 100% соответствует этому интерфейсу.

Вторая реализация, представляющая протокол NativeClient, используя redux-saga (эффекты и каналы), чтобы асинхронный обмен сообщениями выглядел как синхронный вызов функции. Это немного сложно, но работает довольно хорошо и охватывает крайние случаи, с которыми было бы трудно справиться любым другим способом, потому что встроенный обмен сообщениями — это протокол, основанный на стандартных потоках ввода и вывода, поэтому запросы и ответы могут чередоваться, не по порядку и т. д.

Настоящая проблема, которую я не могу решить, заключается в том, что вторая реализация не реализует интерфейс, потому что это генераторы, а не промисы.


В основном хотелось бы преобразовать (обернуть) функцию итератора саги с функцией, возвращающей Promise. Существует хорошая co библиотека, которая в основном делает это для обычных генераторов. Но, похоже, не работает с редукционной сагой.

function* someGenerator() {
  const state = yield select(); // execution freeze here when called from wrapper
  const result = yield call(someEffect);
  return result;
}

function wrapper() {
  return co(someGenerator); // returns Promise
}

Это возможно? Если да, то что я делаю не так?


person mauron85    schedule 08.07.2017    source источник


Ответы (1)


Redux-saga основана на функциях генератора по особой причине — чтобы позволить разделить асинхронные действия на отдельные полученные части и управлять ими из одной конечной точки, которая находится во внутреннем диспетчере процессов саги. Вместо этого, в общем случае, Promise является вещью в себе и не может быть частично выполнен. Другими словами, промисы управляют потоком управления, в котором они расположены, а генераторы управляются внешним потоком управления.

выходной выбор(); // выполнение здесь останавливается при вызове из обёртки

Ваше основное заблуждение состоит в том, что вы предполагаете, что select фактически выполняет какую-то асинхронную операцию. Нет, он просто приостанавливает функцию somegenatator в этой точке и передает управление движку redux-saga, который знает, что делать с возвращаемым значением, и, возможно, констатирует асинхронный процесс (может быть, нет - это не имеет значения). Когда процесс завершен, движок саги возобновляет работу. генератор и передает ему возвращаемое значение.

Вы можете легко увидеть это в исходном коде select (https://github.com/redux-saga/redux-saga/blob/master/src/internal/io.js#L139). Он просто возвращает объект с некоторой структурой, которую может понять движок саги, затем движок выполняет реальное действие и вызывает ваш генератор в формате generatorName.next(resultValue).

УПД. Чисто теоретически, вы можете обернуть его в переназначаемое обещание, но это непригодный для использования случай.

// Your library code

function deferredPromise() {
  let resolver = null;
  const promise = new Promise(resolve => (resolver = resolve));
  return [
    resolver,
    promise
  ];
}

function generateSomeGenerator() {

let [ selectDoneResolve, selectDonePromise ] = deferredPromise();

const someGenetator =  function* () {
  const state = yield select(); // execution freeze here when called from wrapper
  const [newSelectDoneResolve, newSelectDonePromise] = deferredPromise();
  selectDoneResolve({
    info: state, nextPromise: newSelectDonePromise
  });
  selectDoneResolve = newSelectDoneResolve;
  selectDonePromise = newSelectDonePromise;

  const result = yield call(someEffect);
  return result;
}

return {
  someGenetator,
  selectDonePromise
};

}

const { someGenetator: someGenetatorImpl, selectDonePromise } = generateSomeGenerator();

export const someGenetator = someGenetatorImpl;

// Wrapper for interface

selectDonePromise.then(watchDone)

function watchDone({ info, nextPromise }) {
   // Do something with your info
   nextPromise.then(watchDone);
}
person Vladislav Ihost    schedule 10.07.2017
comment
Я понимаю смысл генераторов и редукционной саги. Единственная причина преобразовать генератор в Promise — это абстракция. Представьте себе машинописный интерфейс, в котором все методы возвращают Promise. Одна реализация использует http fetch, который уже основан на промисах, а другая реализация использует генераторы/редукс-сагу внутри, но в соответствии с интерфейсом она должна возвращать промисы. Вот почему мой вопрос, возможно ли преобразование из генераторов (кажется, с использованием co), и являются ли генераторы редукс-саг особым случаем, который не может быть преобразован. И если да то почему. Я добавлю ссылку на файлы проекта, чтобы получить представление... - person mauron85; 11.07.2017
comment
@mauron85 mauron85 Библиотека co не предназначена для общей функции генератора-оболочки в обещании. Вместо этого он просто позволяет писать async/await-подобный код по сериям yield, но в этом коде только то, что делает co-library - это ожидание промисов на верхнем уровне. В случае редукционной саги базовые функции эффектов в генераторах не выполняют никаких реальных действий и передают их на уровень выше, которые в случае co-обертывания ничего не выполняют. - person Vladislav Ihost; 11.07.2017
comment
@ mauron85 Другими словами, сага - это бесконечный прерываемый процесс, который не равен обещанию. Хорошо, предположим, что вы обернули свой код в Promise. Когда это должно решить - person Vladislav Ihost; 11.07.2017
comment
@ mauron85 Теоретически чисто, вы можете обернуть его в переназначаемое обещание, но это непригодный для использования случай pastebin.com/Vda3hxzu - person Vladislav Ihost; 11.07.2017
comment
Я явно упускаю высшую картину, которая скрыта в реализации саг. Но спасибо, что направили меня на правильный путь. Оставив это нерешенным на данный момент. Все еще ищет чистое решение, каким бы оно ни было (например, отказаться от идеи общего интерфейса...). - person mauron85; 11.07.2017