redux-saga を使用して PouchDB から私の React.js アプリケーションにイベントを接続しようとしていますが、理解するのに苦労していますPouchDBから発行されたイベントを私の佐賀に接続する方法。イベントはコールバック関数を使用しているため(そしてジェネレーターを渡すことができないため)、コールバック内でyield put()
を使用できず、ES2015のコンパイル後に(Webpackを使用して)奇妙なエラーが発生します。
これが私が達成しようとしていることです。機能しない部分は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 ]
Nirrekが説明したように、プッシュデータソースに接続する必要がある場合は、そのソース用にイベントイテレータを構築する必要があります。
上記のメカニズムを再利用できるようにすることができることを付け加えたいと思います。したがって、異なるソースごとにイベントイテレータを再作成する必要はありません。
解決策は、put
メソッドとtake
メソッドを使用してジェネリックchannelを作成することです。ジェネレーター内からtake
メソッドを呼び出し、put
メソッドをデータソースのリスナーインターフェイスに接続できます。
これが可能な実装です。誰もメッセージを待っていない場合(たとえば、ジェネレータがリモート呼び出しを行うのに忙しい場合)、チャネルはメッセージをバッファリングすることに注意してください。
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
}
}
そうすれば、上記のチャンネルは、外部のプッシュデータソースを聴きたいときにいつでも使用できます。あなたの例のために
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))
}
}
Redux-sagaの eventChannel
を使用できます
これが私の例です
_// 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);
}
_
注意してください:
eventChannelのメッセージは、デフォルトではバッファリングされません。 _message event
_を1つずつ処理する場合は、const message = yield take(chan);
の後にブロッキング呼び出しを使用することはできません。
または、チャネルのバッファリング戦略を指定するために、eventChannelファクトリにバッファを提供する必要があります(例:eventChannel(subscriber、buffer))。詳細については、 redux-saga API docs を参照してください。
私たちが解決しなければならない基本的な問題は、イベントエミッターが「プッシュベース」であるのに対し、サガは「プルベース」であるということです。
replication.on('change', (info) => {})
のようなイベントをサブスクライブすると、replication
イベントエミッターが新しい値をプッシュすることを決定するたびにコールバックが実行されます。 。
サガでは、コントロールを反転させる必要があります。利用可能な新しい変更情報にいつ応答するかを決定するのは、この物語です。言い換えれば、サガは新しい情報をプルする必要があります。
以下は、これを実現する1つの方法の例です。
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;
}
};
}
上記の例のJSbinがここにあります: http://jsbin.com/cujudes/edit?js,console
同様の質問に対するYassineElouafiの回答も確認する必要があります: redux-sagaのes6ジェネレーターをwebsocketまたはeventsourceのonmessageリスナーとして使用できますか?
@YassineElouafiに感謝します
@Yassine Elouafiによるソリューションに基づいて、TypeScript言語のredux-saga拡張機能として短いMITライセンスの一般チャネル実装を作成しました。
// 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
};
}
そして、redux-saga * takeEvery構文に似た使用例
// 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;
}
PouchDBを使用しても同じ問題が発生し、提供された回答が非常に有用で興味深いことがわかりました。しかし、PouchDBで同じことを行う方法はたくさんあり、私は少し掘り下げて、おそらく推論しやすい別のアプローチを見つけました。
リスナーを_db.change
_リクエストにアタッチしない場合、変更データは呼び出し元に直接返され、オプションに_continuous: true
_を追加すると、ロングポールが発行され、何らかの変更が発生するまで返されません。したがって、次の場合でも同じ結果が得られます。
_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})
}
}
}
_
私が底に達していないことが1つあります。 for
ループをchange.results.forEach((change)=>{...})
に置き換えると、yield
で無効な構文エラーが発生します。イテレータの使用における衝突と関係があると思います。