J'essaie d'utiliser redux-saga pour connecter des événements de PouchDB à mon React.js , mais j'ai du mal à comprendre comment relier les événements émis par PouchDB à mon Saga. Étant donné que l'événement utilise une fonction de rappel (et que je ne peux pas lui attribuer de générateur), je ne peux pas utiliser yield put()
dans le rappel, cela génère des erreurs étranges après la compilation ES2015 (à l'aide de Webpack).
Alors voici ce que j'essaie d'accomplir, la partie qui ne fonctionne pas est dans replication.on('change' (info) => {})
.
function * startReplication (wrapper) {
while (yield take(DATABASE_SET_CONFIGURATION)) {
yield call(wrapper.connect.bind(wrapper))
// Returns a promise, or false.
let replication = wrapper.replicate()
if (replication) {
replication.on('change', (info) => {
yield put(replicationChange(info))
})
}
}
}
export default [ startReplication ]
Comme Nirrek l'a expliqué, lorsque vous devez vous connecter à des sources de données Push, vous devez créer un itérateur d'événementpour cette source.
Je voudrais ajouter que le mécanisme ci-dessus pourrait être rendu réutilisable. Il n'est donc pas nécessaire de recréer un itérateur d'événement pour chaque source différente.
La solution consiste à créer un channel générique avec les méthodes put
et take
. Vous pouvez appeler la méthode take
depuis l'intérieur du générateur et connecter la méthode put
à l'interface d'écoute de votre source de données.
Voici une implémentation possible. Notez que le canal met les messages en mémoire tampon si personne ne les attend (par exemple, le générateur est en train de faire un appel distant)
function createChannel () {
const messageQueue = []
const resolveQueue = []
function put (msg) {
// anyone waiting for a message ?
if (resolveQueue.length) {
// deliver the message to the oldest one waiting (First In First Out)
const nextResolve = resolveQueue.shift()
nextResolve(msg)
} else {
// no one is waiting ? queue the event
messageQueue.Push(msg)
}
}
// returns a Promise resolved with the next message
function take () {
// do we have queued messages ?
if (messageQueue.length) {
// deliver the oldest queued message
return Promise.resolve(messageQueue.shift())
} else {
// no queued messages ? queue the taker until a message arrives
return new Promise((resolve) => resolveQueue.Push(resolve))
}
}
return {
take,
put
}
}
Ensuite, le canal ci-dessus peut être utilisé chaque fois que vous souhaitez écouter une source de données Push externe. Pour votre exemple
function createChangeChannel (replication) {
const channel = createChannel()
// every change event will call put on the channel
replication.on('change', channel.put)
return channel
}
function * startReplication (getState) {
// Wait for the configuration to be set. This can happen multiple
// times during the life cycle, for example when the user wants to
// switch database/workspace.
while (yield take(DATABASE_SET_CONFIGURATION)) {
let state = getState()
let wrapper = state.database.wrapper
// Wait for a connection to work.
yield apply(wrapper, wrapper.connect)
// Trigger replication, and keep the promise.
let replication = wrapper.replicate()
if (replication) {
yield call(monitorChangeEvents, createChangeChannel(replication))
}
}
}
function * monitorChangeEvents (channel) {
while (true) {
const info = yield call(channel.take) // Blocks until the promise resolves
yield put(databaseActions.replicationChange(info))
}
}
Nous pouvons utiliser eventChannel
of redux-saga
Voici mon exemple
// fetch history messages
function* watchMessageEventChannel(client) {
const chan = eventChannel(emitter => {
client.on('message', (message) => emitter(message));
return () => {
client.close().then(() => console.log('logout'));
};
});
while (true) {
const message = yield take(chan);
yield put(receiveMessage(message));
}
}
function* fetchMessageHistory(action) {
const client = yield realtime.createIMClient('demo_uuid');
// listen message event
yield fork(watchMessageEventChannel, client);
}
Veuillez noter :
les messages sur un eventChannel ne sont pas mis en mémoire tampon par défaut. Si vous souhaitez traiter message event
uniquement un par un, vous ne pouvez pas utiliser d'appel bloquant après const message = yield take(chan);
.
Ou Vous devez fournir un tampon à la fabrique eventChannel afin de spécifier une stratégie de mise en mémoire tampon pour le canal (par exemple, eventChannel (abonné, tampon)). Voir Documents de l'API redux-saga pour plus d'informations
Le problème fondamental que nous devons résoudre est que les émetteurs d'événements sont «basés sur Push», alors que les sagas sont «basées sur pull».
Si vous vous abonnez à un événement tel que: replication.on('change', (info) => {})
, le rappel est exécuté chaque fois que l'émetteur d'événements replication
décide de Push une nouvelle valeur.
Avec les sagas, nous devons inverser le contrôle. C'est la saga qui doit contrôler le moment où elle décide de répondre aux nouvelles informations de changement disponibles. En d'autres termes, une saga doit pull la nouvelle information.
Vous trouverez ci-dessous un exemple de solution:
function* startReplication(wrapper) {
while (yield take(DATABASE_SET_CONFIGURATION)) {
yield apply(wrapper, wrapper.connect);
let replication = wrapper.replicate()
if (replication)
yield call(monitorChangeEvents, replication);
}
}
function* monitorChangeEvents(replication) {
const stream = createReadableStreamOfChanges(replication);
while (true) {
const info = yield stream.read(); // Blocks until the promise resolves
yield put(replicationChange(info));
}
}
// Returns a stream object that has read() method we can use to read new info.
// The read() method returns a Promise that will be resolved when info from a
// change event becomes available. This is what allows us to shift from working
// with a 'Push-based' model to a 'pull-based' model.
function createReadableStreamOfChanges(replication) {
let deferred;
replication.on('change', info => {
if (!deferred) return;
deferred.resolve(info);
deferred = null;
});
return {
read() {
if (deferred)
return deferred.promise;
deferred = {};
deferred.promise = new Promise(resolve => deferred.resolve = resolve);
return deferred.promise;
}
};
}
Il y a un JSbin de l'exemple ci-dessus ici: http://jsbin.com/cujudes/edit?js,console
Vous devriez également jeter un coup d'œil à la réponse de Yassine Elouafi à une question similaire: Puis-je utiliser les générateurs es6 de redux-saga en tant qu'auditeur sur message pour websockets ou eventsource?
Merci à Yassine Elouafi
J'ai créé une courte implémentation de _ MIT canaux généraux sous licence comme extension redux-saga pour le langage TypeScript basé sur la solution de @Yassine Elouafi.
// redux-saga/channels.ts
import { Saga } from 'redux-saga';
import { call, fork } from 'redux-saga/effects';
export interface IChannel<TMessage> {
take(): Promise<TMessage>;
put(message: TMessage): void;
}
export function* takeEvery<TMessage>(channel: IChannel<TMessage>, saga: Saga) {
while (true) {
const message: TMessage = yield call(channel.take);
yield fork(saga, message);
}
}
export function createChannel<TMessage>(): IChannel<TMessage> {
const messageQueue: TMessage[] = [];
const resolveQueue: ((message: TMessage) => void)[] = [];
function put(message: TMessage): void {
if (resolveQueue.length) {
const nextResolve = resolveQueue.shift();
nextResolve(message);
} else {
messageQueue.Push(message);
}
}
function take(): Promise<TMessage> {
if (messageQueue.length) {
return Promise.resolve(messageQueue.shift());
} else {
return new Promise((resolve: (message: TMessage) => void) => resolveQueue.Push(resolve));
}
}
return {
take,
put
};
}
Et exemple d'utilisation similaire à redux-saga * takeEvery construction
// example-socket-action-binding.ts
import { put } from 'redux-saga/effects';
import {
createChannel,
takeEvery as takeEveryChannelMessage
} from './redux-saga/channels';
export function* socketBindActions(
socket: SocketIOClient.Socket
) {
const socketChannel = createSocketChannel(socket);
yield* takeEveryChannelMessage(socketChannel, function* (action: IAction) {
yield put(action);
});
}
function createSocketChannel(socket: SocketIOClient.Socket) {
const socketChannel = createChannel<IAction>();
socket.on('action', (action: IAction) => socketChannel.put(action));
return socketChannel;
}
J'avais le même problème en utilisant également PouchDB et trouvais les réponses fournies extrêmement utiles et intéressantes. Cependant, il y a beaucoup de façons de faire la même chose dans PouchDB et j'ai fouillé un peu et trouvé une approche différente qu'il est peut-être plus facile de raisonner.
Si vous n'attachez pas de programmes d'écoute à la demande db.change
, toutes les données de modification sont directement renvoyées à l'appelant. L'ajout de continuous: true
à l'option entraîne l'émission d'un long défilement et ne revient pas tant qu'un changement n'est pas intervenu. Donc, le même résultat peut être obtenu avec ce qui suit
export function * monitorDbChanges() {
var info = yield call([db, db.info]); // get reference to last change
let lastSeq = info.update_seq;
while(true){
try{
var changes = yield call([db, db.changes], { since: lastSeq, continuous: true, include_docs: true, heartbeat: 20000 });
if (changes){
for(let i = 0; i < changes.results.length; i++){
yield put({type: 'CHANGED_DOC', doc: changes.results[i].doc});
}
lastSeq = changes.last_seq;
}
}catch (error){
yield put({type: 'monitor-changes-error', err: error})
}
}
}
Il y a une chose que je n'ai pas eue au fond. Si je remplace la boucle for
par change.results.forEach((change)=>{...})
, une erreur de syntaxe non valide se produit sur yield
. Je suppose que cela a quelque chose à voir avec certains conflits dans l'utilisation des itérateurs.