Resubscribing happens only after triggering the source observable the second time - javascript

I'm trying to compose a stream with a send event and an undo event. after sending the message, there is a 3s delay while you can undo the message and return the sent message into the text field. if you started to compose a new message, the sent message should be prepended.
so far I've managed to create the delayed send and undo functionality. the problem occurs, when I send a message, undo it, and then send it again without touching the input, I need to change the value of the input to be able to re-send the message, but cannot resend the restored message.
tried a few workarounds, like dispatching an input event on the textarea, or calling next on the message observable, both in the restore function. none of them worked.
textarea.addEventListener('input', event => message$.next(event.target.value))
send.addEventListener('click', () => textarea.value = '')
const sendEvent$ = fromEvent(send, 'click')
const undoEvent$ = fromEvent(undo, 'click')
const message$ = new Subject()
let cache = []
sendEvent$
.pipe(
withLatestFrom(message$, (_, m) => m),
tap(m => cache.push(m)),
delay(3000),
takeUntil(undoEvent$.pipe(
tap(restore)
)),
repeatWhen(complete => complete)
)
.subscribe(x => {
console.log(x)
cache = []
})
function restore() {
if (!textarea.value) {
const message = cache.join('\n')
textarea.value = message
cache = []
}
}
link the example: https://stackblitz.com/edit/rxjs-undo-message

The problem is pretty much there in the restore function. When you restore the "onInput" event doesn't get triggered. So your message queue basically is not enqueued with the restored item. The suggestion given by #webber where you can pass the message$.next(message) is pretty much right and that's what you need to do.
But the problem is how exactly you set it. You can set the value through a setTimeout interval in restore() so that your takeUntil() completes and then the value is enqueued in the Subject
function restore() {
if (!textarea.value) {
const message = cache.join('\n')
textarea.value = message
cache = []
setTimeout(function(){
message$.next(message)
},3000)
}
}
(or)
You can remove the
textarea.addEventListener('input', event => message$.next(event.target.value))
and change your send event handler to the following.
send.addEventListener('click', () => {
message$.next(textarea.value);
textarea.value =''
})

The subscriber works fine, it's just that your message$ doesn't get updated when the undoEvent$ triggers. However the value gets set to an empty string.
If you undo, then type and then send again, you will see that it works in the first time as well.
What you have to do is set message$ to the value of your textarea and then it works.

Related

SolidJS: How to trigger refetch of createResource?

I have a createResource which is working:
const fetchJokes = async (programmingOnly) => {
alert(programmingOnly);
return (await fetch(`https://official-joke-api.appspot.com/jokes/${programmingOnly?'programming/':''}ten`)).json();
}
//...
const [jokes, { mutate, refetch }] = createResource(programmingOnly(), fetchJokes);
Now I want to change the programmingOnly boolean via it's signal:
const [programmingOnly, setProgrammingOnly] = createSignal(true);
//...
Programming Only: <input type="checkbox" checked={programmingOnly()}
onInput={()=>{setProgrammingOnly(!programmingOnly());refetch();}}> </input>
But this does not work, the alert fires upon subsequent attempts, but with undefined as arg, and nothing happens with the data.
What is the SolidJS way of approaching this?
I believe the problem here is the signal is getting set with false every time. Since programmingOnly is a signal it should be accessed as a function to retrieve its value.
Ie..
setProgrammingOnly(!programmingOnly)
// should be
setProgrammingOnly(!programmingOnly())
You should not need to call refetch or do anything else for the example to work.

React state disappearing

I have a react app that uses the MS Graph API (so it's a bit difficult to post a minimal reproducible example). It has a state variable called chats that is designed to hold the result of fetching a list of chats from the graph API. I have to poll the API frequently to get new chats.
I query the chats endpoint, build an array of newChats and then setChats. I then set a timeout that refreshes the data every 10 seconds (it checks for premature invocation through the timestamp property stored in the state). If the component is unmounted, a flag is set, live (useRef), which stops the refresh process. Each chat object is then rendered by the Chat component (not shown).
Here's the code (I've edited by hand here to remove some irrelevant bits around styles and event propagation so it's possible that typo's have crept in -- it compiles and runs in reality).
const Chats = () => {
const [chats, setChats] = useState({ chats: [], timestamp: 0 });
const live = useRef(true);
const fetchChats = () => {
if (live.current && Date.now() - chats.timestamp < 9000) return;
fetchData(`${baseBeta}/me/chats`).then(res => {
if (res.value.length === chats.chats.length) return;
const chatIds = chats.chats.map(chat => chat.id);
const newChats = res.value.filter(chat => !chatIds.includes(chat.id));
if (newChats.length > 0) {
setChats(c => ({ chats: [...c.chats, ...newChats], timestamp: Date.now() }));
}
setTimeout(fetchChats, 10000);
});
};
useEffect(() => {
fetchChats();
return () => (live.current = false);
}, [chats]);
return (
<div>
{chats.chats.map(chat => (
<Chat chat={chat} />
))}
</div>
);
};
The Chat component must also make some async calls for data before it is rendered.
This code works, for a second or two. I see the Chat component rendered on the screen with the correct details (chat member names, avatars, etc.), but almost before it has completed rendering I see the list elements being removed, apparently one at a time, though that could just be the way its rendered -- it could be all at once. The list collapses on the screen, showing that the chat state has been cleared out. I don't know why this is happening.
I've stepped through the code in the debugger and I can see the newChats array being populated. I can see the setChats call happen. If I put a breakpoint on that line then it is only invoked once and that's the only line that sets that particular state.
So, what's going on? I'm pretty sure React isn't broken. I've used it before without much trouble. What's changed recently is the inclusion of the refresh code. I'm suspicious that the reset is taking away the state. My understanding is that the fetchChats method will be rendered every time the chats state changes and so should see the current value of the chats state. Just in case this wasn't happening, I passed the chats state from the useEffect like this:
useEffect(() => {
fetchChats(chats);
return () => (live.current = false);
}, [chats]);
With the necessary changes in fetchChats to make this work as expected. I get the same result, the chats state is lost after a few seconds.
Edit
Still Broken:
After #Aleks answer my useEffect now looks like this:
useEffect(() => {
let cancel = null;
let live = true;
const fetchChats = () => {
if (Date.now() - chats.timestamp < 9000) return;
fetchData(`${baseBeta}/me/chats`).then(res => {
if (res.value.length === chats.chats.length) return;
const chatIds = chats.chats.map(chat => chat.id);
const newChats = res.value.filter(chat => chat.chatType === "oneOnOne" && !chatIds.includes(chat.id));
if (newChats.length > 0 && live) {
setChats(c => ({ chats: [...c.chats, ...newChats], timestamp: Date.now() }));
}
cancel = setTimeout(fetchChats, 10000);
});
};
fetchChats();
return () => {
live = false;
cancel?.();
};
}, []);
The result of this is that the chats are loaded, cleared, and loaded again, repeatedly. This is better, at least they're reloading now, whereas previously they would disappear forever. They are reloaded every 10 seconds, and cleared out almost immediately still.
Eventually, probably due to random timings in the async calls, the entries in the list are duplicated and the 2nd copy starts being removed immediately instead of the first copy.
There are multiple problems. First this
setTimeout(fetchChats, 10000); will trigger
useEffect(() => {
fetchChats(chats);
return () => (live.current = false);
}, [chats])
You will get 2 fetches one after another.
But the bug you're seeing is because of this
return () => (live.current = false);
On second useEffect trigger, clean up function above with run and live.current will be forever false from now on.
And as Nikki9696 said you you need to clear Timeout in clean up function
The easiest fix to this is, probably
useEffect(() => {
let cancel = null;
let live = true;
const fetchChats = () => {
// not needed
//if ( Date.now() - chats.timestamp < 9000) return;
fetchData(`${baseBeta}/me/chats`).then(res => {
//this line is not needed
//if (res.value.length === chats.chats.length) return;
// remove all the filtering, it can be done elsewhere where
// you can access fresh chat state
//const chatIds = chats.chats.map(chat => chat.id);
//const newChats = res.value.filter(chat =>
//!chatIds.includes(chat.id));
if (res.value?.length > 0&&live) {
setChats(c => ({ chats: [...c.chats, ...res.value], timestamp: Date.now() }));
cancel = setTimeout(fetchChats, 10000);
}
});
};
fetchChats()
return () => { live=false; if(cancel)window.clearTimeout(cancel) };
}, []);
Edit: typo cancel?.() to window.clearTimeout(cancel);
Ok, I have an idea what's happening and how to fix it. I am still not sure why it is behaving like this, so please comment if you understand it better than me.
Basically, for some reason I don't understand, the function fetchChats only ever sees the initial state of chats. I am making the mistake of filtering my newly fetched list against this state, in which the array is empty.
If I change my useEffect code to do this instead:
setChats(c => {
return {
chats: [
...c.chats,
...res.value.filter(cc => {
const a = c.chats.map(chat => chat.id);
return !a.includes(cc.id);
})
],
timestamp: Date.now()
};
});
Then my filter is passed the current value of the state for chats rather than the initial state.
I thought that because the function containing this code is in the function that declares the chat state, whenever that state changed the whole function would be rendered with the new value of chats making it available to its nested functions. This isn't the case here and I don't understand why.
The solution, to only trust the values of the state that is handed to me during the setState (setChats) call, works fine and I'll go with it, but I'd love to know what is wrong with reading the state directly.

How to make this run only once?

I'm trying to get it where If someone clicks the button it will update the database but what happens if I enter 50 then it will keep running it and I have a tracking board that sums everything up so it overloads my server and makes the total in the 1000's when its normally just over 100.
I've tried a document ready function, I've had on and one. ('click') and it keeps running multiple times
$('#update_new_used-counter').one('click', function (event) {
event.preventDefault();
let updated_new_counter = $('#new_sold-update').val().trim();
let updated_used_counter = $('#used_sold-update').val().trim();
trackingBoardRef.on("value", function(snapshot) {
let current_new_counter = snapshot.val().new;
let current_used_counter = snapshot.val().used;
if (updated_new_counter == '') {
trackingBoardRef.update({
new: current_new_counter,
});
} else {
trackingBoardRef.update({
new: updated_new_counter,
})
};
if (updated_used_counter == '') {
trackingBoardRef.update({
used: current_used_counter,
});
} else {
trackingBoardRef.update({
used: updated_used_counter,
})
};
console.log(snapshot.val().new);
console.log(snapshot.val().used);
});
});
That's what I have now and it just keeps running multiple times until firebase says I had to many requests and stops it. I just want it to update once
When you call:
trackingBoardRef.on("value", function(snapshot) {
You attach a listener to the data in trackingBoardRef that will be triggered right away with the current value, and then subsequently whenever the data under trackingBoardRef changes. And since you're changing data under trackingBoardRef in your code, you're creating an infinite loop.
If you only want to read the data once, you can use the aptly named once method:
trackingBoardRef.once("value", function(snapshot) {
...
Note that if you're update the value under trackingBoardRef based on its current value, you really should use a transaction to prevent users overwriting each other's changes.

RxJS not all Subscribers receive all events

I'm working on an exercise about RxJS.
And there's something very strange happening:
typoStream.subscribe(x => console.log('wont get executed'));
wordCompletedStream.subscribe(nextStream);
typoStream.subscribe(x => console.log('will get executed'));
When the application runs the first console.log won't get printed and the second one will.
Regardless of what the streams are and how they interact - this should never happen, right? Why is it important when I subscribe to an observable - shouldn't it emit the event to every subscriber anyways?
If you want to try it:
http://embed.plnkr.co/xb8Yimo5RcYGPtgClYgY/
Type the displayed word correctly and you can see the "error" in action. But it doesn't happen every time - only most of the time.
Here's the stream flow: https://photos.app.goo.gl/Z4cpKzekAIuKzMF93
I had a play with the code you posted, and the key fix is to properly multicast the checkWord observable. You can do this with .publish().refCount() like you did for wordStream, or you can use the shortcut method that does the same thing, .share().
const checkStream = wordStream.combineLatest(inputStream).share();
The reason this works is that without it, multiple subscriptions to checkStream or any streams derived from it, such as typoStream and wordCompletedStream will each trigger a new subscription to the wordStream observable (which is correctly multicast, so no new request gets made) and the inputStream observable, which will register new event listeners on the input.
With the .share() operator, it doesn't matter how many subscriptions are made to checkStream or derived observables, only the first one will trigger a subscription to inputStream.
Note that after this fix, neither of the two subscribers to typoStream will fire for a correctly entered word. Which is what I would expect from an observable called typoStream. Both will fire when an incorrect character is entered.
Forked Plunkr here
Or see the snippet below:
(() => {
// --- UI Stuff, NO NEED TO TOUCH THESE --- //
const wordField = $('#TotDWord');
const inputField = $('#TotDInput');
// ----------------------------------------- //
// A stream of the users string inputs
const inputFieldStream = Rx.Observable.fromEvent(inputField, 'keyup')
.map(x => x.target.value).distinctUntilChanged();
// This stream is used to represent the users unput - we don't use the
// inputFieldStream directly because we want to manually send values aswell
const inputStream = new Rx.Subject();
// Feed the stream from the field into our inputStream
inputFieldStream.subscribe(inputStream);
// A stream that allows us to manually trigger that we need a new word
const nextStream = new Rx.Subject();
// When we want the next word we need to reset the users input
nextStream.subscribe(() => {
inputField.val('');
inputStream.onNext('');
});
// This stream calls a server for a new random word every time the nextStream emits an event. We startWith a value to trigger the first word
const wordStream = nextStream.startWith('')
.flatMapLatest(getRandomWord)
// publish & refCount cache the result - otherwise every .map on wordStream would cause a new HTTP request
.publish().refCount();
// When there is a new word, we display it
wordStream.subscribe(word => {
wordField.empty();
wordField.append(word);
});
// Checkstream combines the latest word with the latest userinput. It emits an array, like this ['the word', 'the user input'];
const checkStream = wordStream.combineLatest(inputStream).share();
// Emits an event if the user input is not correct
const typoStream = checkStream.filter(tuple => {
const word = tuple[0];
const input = tuple[1];
return !word.startsWith(input);
});
// When there is a typo we need a new word
typoStream.subscribe(nextStream);
// Emits an event when the user has entered the entire word correctly
const wordCompletedStream = checkStream.filter(tuple => {
const word = tuple[0];
const input = tuple[1];
return word == input;
});
/**
* THIS WILL (MOST OF THE TIME) NOT FIRE WHEN YOU COMPLETE A WORD
*/
typoStream.subscribe(x => console.log('wont get executed'));
// Whenever the word is completed, request a new word
wordCompletedStream.subscribe(nextStream);
/**
* THIS WILL FIRE WHEN YOU COMPLETE A WORD
*/
typoStream.subscribe(x => console.log('will get executed'));
// Calls a server for a random word
// returns a promise
function getRandomWord() {
return $.ajax({
// Change the URL to cause a 404 error
url: 'https://setgetgo.com/randomword/get.php'
}).promise();
}
})();
<script data-require="jquery" data-semver="3.1.1" src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.0.6/rx.all.js"></script>
<div>
<h1>Exercise: Typing of the Dead</h1>
<div>
Type the given word correctly and watch the console. Most of the time 1 of the 2 subscriptions on the typoStream will fire (when there should fire none).
</div>
<br />
<div id="TotDWord"></div>
<input type="text" name="" id="TotDInput" value="" /><span>Highscore: </span><span id="TotDHighscore"></span>
<div id="TotDScore"></div>
</div>
<script>
console.clear();
</script>

How to execute dom manipulation from a subscriber without side effect RxJS?

Scenario
I have a web component where the DOM manipulation is handled internally and not exposed to the outside world. The outside world has access to a stream that the web component provides.
Every time the web component emits a valid value, internally it should clear the value from the input component. However, this appears to have side effect on the stream.
Questions
Why does this happen?
How can clear subscription be defined without side effect on other subscribers?
Code
const logExternally = createFakeComponentStream()
.subscribe(logValue);
function createFakeComponentStream() {
const inputStream = Rx.Observable.fromEvent(
document.querySelector("[name='input']"),
'keyup')
.filter(event => /enter/i.test(event.key));
const valueStream = inputStream
.pluck('srcElement', 'value');
const logInternally = valueStream.subscribe(logValue);
const clearOnInput = inputStream
.pluck('srcElement')
.subscribe(clearInput);
return valueStream;
}
function clearInput(input) {
input.value = '';
}
function logValue(value) {
if (value) {
console.log('Success:', value);
} else {
console.log('Failed:', value);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.4.2/Rx.js"></script>
<input type="text" name="input" value="" />
Expected Output
Success: asdf
Success: asdf
Actual Output
Success: asdf
Failed:
You are passing the DOM element through the observable streams and one of your subscribers is mutating the DOM element, so when the 2nd observer receives the notification and checks the value of the DOM element, it has already been modified.
To avoid this, you need to capture the value before you clear the input. something like this:
const inputStream = Rx.Observable.fromEvent(
document.querySelector("[name='input']"),
'keyup')
.filter(event => /enter/i.test(event.key))
.map(event => ({ srcElement: event.srcElement, value: event.srcElement.value }))
.share();
const valueStream = inputStream.pluck("value");
const logInternally = valueStream.subscribe(logValue);
const clearOnInput = inputStream.pluck("srcElement").subscribe(clearInput);
return valueStream;
I made 2 changes:
I use map to capture the value of the DOM Element early in the stream
I use share so that this captured value is shared with all subsequent subscriptions to the input stream
These two changes will shield the valueStream subscribers from the clearInput mutation.

Categories

Resources