How to use debounce stream based on value? - rxjs

For example, assume that we have stream like following
Stream 1 | -1-2-3-1-2-3--4-----------
after debounce, I would like to have the emitted stream looks like as follows:
Stream 2 | ---------------1-2-3--4------
There are lots of examples how to debounce the stream, but they take all value as the same trigger.
The following is the example code I found in reactitve-extension website,
var Rx = require('rxjs/Rx');
var times = [
{ value: 1, time: 100 },
{ value: 2, time: 200 },
{ value: 3, time: 300 },
{ value: 1, time: 400 },
{ value: 2, time: 500 },
{ value: 3, time: 600 },
{ value: 4, time: 800 }
];
// Delay each item by time and project value;
var source = Rx.Observable.from(times)
.flatMap(function (item) {
return Rx.Observable
.of(item.value)
.delay(item.time);
})
.debounceTime(500 /* ms */);
var subscription = source.subscribe(
function (x) {
console.log('Next: %s', x);
},
function (err) {
console.log('Error: %s', err);
},
function () {
console.log('Completed');
});
The console output would be
Next: 4
Completed
But I would like to get the following output
Next: 1
Next: 2
Next: 3
Next: 4
Completed
Maxime give good answer.
I also try myself. Hope help someone who have the same question.
var Rx = require('rxjs/Rx');
var times = [
{ value: 1, time: 100 },
{ value: 2, time: 200 },
{ value: 3, time: 300 },
{ value: 1, time: 400 },
{ value: 2, time: 500 },
{ value: 3, time: 600 },
{ value: 4, time: 800 },
{ value: 5, time: 1500 }
];
// Delay each item by time and project value;
var source = Rx.Observable.from(times)
.flatMap(function (item) {
return Rx.Observable
.of(item.value)
.delay(item.time);
})
.do(obj => console.log('stream 1:', obj, 'at', Date.now() - startTime, `ms`))
.groupBy(obj => obj)
.flatMap(group => group.debounceTime(500))
let startTime = Date.now();
var subscription = source.subscribe(
function (x) {
console.log('stream 2: %s', x, 'at', Date.now() - startTime, 'ms');
},
function (err) {
console.log('Error: %s', err);
},
function () {
console.log('Completed');
});
The console will output
stream 1: 1 at 135 ms
stream 1: 2 at 206 ms
stream 1: 3 at 309 ms
stream 1: 1 at 409 ms
stream 1: 2 at 509 ms
stream 1: 3 at 607 ms
stream 1: 4 at 809 ms
stream 2: 1 at 911 ms
stream 2: 2 at 1015 ms
stream 2: 3 at 1109 ms
stream 2: 4 at 1310 ms
stream 1: 5 at 1510 ms
stream 2: 5 at 1512 ms
Completed

Here's the code I propose :
const { Observable } = Rx
const objs = [
{ value: 1, time: 100 },
{ value: 2, time: 200 },
{ value: 3, time: 300 },
{ value: 1, time: 400 },
{ value: 2, time: 500 },
{ value: 3, time: 600 },
{ value: 4, time: 800 }
];
const tick$ = Observable.interval(100)
const objs$ = Observable.from(objs).zip(tick$).map(x => x[0])
objs$
.groupBy(obj => obj.value)
.mergeMap(group$ =>
group$
.debounceTime(500))
.do(obj => console.log(obj))
.subscribe()
And the output is just as expected :
Here's a working Plunkr with demo
https://plnkr.co/edit/rEI8odCrhp7GxmlcHglx?p=preview
Explanation :
I tried to make a small schema :
The thing is, you cannot use the debounceTime directly on the main observable (that's why you only had one value). You've got to group every values in their own stream with the groupBy operator and apply the debounceTime to the splitted group of values (as I tried to show in the image). Then use flatMap or mergeMap to get one final stream.
Doc :
Here are some pages that might help you understand :
- groupBy
- debounceTime
- mergeMap

Related

Cypress - Add a text to input area using lorem-ipsum

I am adding a random text to input using lorem-ipsum in cypress. Does anyone know how to handle this in cypress?
I am using following method and going to call addComment() method in the test
import { loremIpsum } from "lorem-ipsum";
addcomment() {
loremIpsum({
count: 1, // Number of "words", "sentences", or "paragraphs"
format: "plain", // "plain" or "html"
paragraphLowerBound: 3, // Min. number of sentences per paragraph.
paragraphUpperBound: 7, // Max. number of sentences per paragarph.
random: Math.random, // A PRNG function
sentenceLowerBound: 5, // Min. number of words per sentence.
sentenceUpperBound: 15, // Max. number of words per sentence.
suffix: "\n", // Line ending, defaults to "\n" or "\r\n" (win32)
units: "sentences", // paragraph(s), "sentence(s)", or "word(s)"
})
cy.get('.input > textarea').loremIpsum()
}
Assuming you have already installed the plugin. Then your test should look like this:
import {LoremIpsum} from 'lorem-ipsum'
const lorem = new LoremIpsum({
sentencesPerParagraph: {
max: 8,
min: 4,
},
wordsPerSentence: {
max: 16,
min: 4,
},
})
describe('test suite', () => {
it('Test case', () => {
//Types 5 Sentences
cy.get('.input > textarea').type(lorem.generateSentences(5))
//Types 7 Paragraphs
cy.get('.input > textarea').type(lorem.generateParagraphs(7))
})
})
Install the plugin
npm i lorem-ipsum
Then use the above code mentioned by Alapan Das.
My code
import {LoremIpsum} from 'lorem-ipsum'
addcomment2() {
const lorem = new LoremIpsum({
sentencesPerParagraph: {
max: 8,
min: 4
},
wordsPerSentence: {
max: 16,
min: 4
}
});
let txtComment = lorem.generateSentences(3);
cy.get(".input > textarea").type(txtComment);
}

fetch in parallel async/await with Promises.all

I continue to struggle with serial/parallel processing in JS (promises). I want to query my server and identify those queries that take longer than, say 500 ms. The following works, but as far as I understand, the queries are made one after another.
const query = async (queries) => {
for (let i = 0, j = queries.length; i < j; i++) {
let t = process.hrtime();
const response = await fetch(queries[i]);
const result = await response.json();
t = process.hrtime(t);
const ms = Math.round((t[0] * 1000) + (t[1] / 1000000));
if (ms > 500) {
console.log(ms, queries[i]);
}
}
}
query(arrayOfQueries);
// console output below (snipped for brevity)
3085 http://localhost:3010/v3/…
2463 http://localhost:3010/v3/…
2484 http://localhost:3010/v3/…
…
I change the above code to fire the queries in parallel, but now I get back an array of promises. I can't figure out how to identify only those promises that take longer than 500ms to resolve
const query = async (queries) => {
const r = await Promise.all(queries.map(async (q) => fetch(q)));
console.log(r);
};
// console output
[
Response {
size: 0,
timeout: 0,
[Symbol(Body internals)]: { body: [PassThrough], disturbed: false, error: null },
[Symbol(Response internals)]: {
url: 'http://localhost:3010/v3/…',
status: 200,
statusText: 'OK',
headers: [Headers],
counter: 0
}
},
Response {
size: 0,
timeout: 0,
[Symbol(Body internals)]: { body: [PassThrough], disturbed: false, error: null },
[Symbol(Response internals)]: {
url: 'http://localhost:3010/v3/…',
status: 200,
statusText: 'OK',
headers: [Headers],
counter: 0
}
},
Response {
size: 0,
timeout: 0,
[Symbol(Body internals)]: { body: [PassThrough], disturbed: false, error: null },
[Symbol(Response internals)]: {
url: 'http://localhost:3010/v3/…',
status: 200,
statusText: 'OK',
headers: [Headers],
counter: 0
}
},
When running the queries in parallel, you would have to add code (similar to what you had for your non-parallel example) to time each one separately so you could track each individual request separately.
The time of each request overlaps so you can't keep track of the timing of each individual request from the outside. Here's an example of timing each individual request:
const query = async (queries) => {
const r = await Promise.all(queries.map(async (q) => {
const start = Date.now();
const response = await fetch(q);
const json = await response.json();
const delta = Date.now() - start;
console.log(`${delta}ms for ${q}`);
return json;
});
return r;
};
This will output the timing for each request at the time it finishes which may not be in the same order that the requests were made. If you want, you can collect these timing results into an array and output all the timing at once at the end.

Create a good k6 load test with graphql/subscription

After struggling against k6, with the help of the answers to my other question, i was able (i think) to test subscriptions with websocket.
Now i'm trying to desing a good test. Our app is a Single Page App that mainly uses websocket subscriptions instead of queries. The problem is that i'm a bit lost on how to define how many subscriptiosn i should test in order to detect if my app will be able to support around 800 users working simultaneously in real time. (I started testing it first with a top of 150 VUs).
For now, i'm running the test with 3 commonly used subscriptions in our user path, but:
Should I try more subscriptions, trying to cover the most used ones in our user path, or should I keep it simple and pick the 3 most used ones and add a timeout between them?
Is this a correct approach for load/stress testing with GRAPHQL subscriptions/websocket? I'm not sure if i'm being too cautious about this, but i'm afraid of giving a false status about our situation.
I am a bit confused on how to interpret the results screen, especially on how I can infer if we will be able to give a good experience to our users. Should I take the avg and p(95) of ws_ping as a reference for this?
k6 screen result
As a reference, here is part of the code i'm using to perform the test
Thanks in advance!
main.js
import { Httpx } from 'https://jslib.k6.io/httpx/0.0.5/index.js';
const session = new Httpx({
baseURL: `https://${enviorment}`
});
const wsUri = `wss://${enviorment}/v1/graphql`;
const pauseMin = 2;
const pauseMax = 6;
export const options = {
stages: [
{ duration: '30s', target: 50 },
{ duration: '30s', target: 100 },
{ duration: '30s', target: 150 },
{ duration: '120s', target: 150 },
{ duration: '60s', target: 50 },
{ duration: '30s', target: 0 },
]
};
export default function () {
session.addHeader('Content-Type', 'application/json');
todas(keys.profesorFirebaseKey, keys.desafioCursoKey, keys.nivelCurso, keys.profesorKey)
}
todas.js:
import ws from 'k6/ws';
import {fail,check} from 'k6'
import exec from 'k6/execution';
export function todas(id, desafioCursoKey, nivel, profesorKey) {
const queryList = [`];
let ArraySubscribePayload = []
for (let i = 0; i < 3; i++) {
let subscribePayload = {
id: String(1 + 3 * i),
payload: {
extensions: {},
query: queryList[i],
variables: {},
},
type: "start",
}
ArraySubscribePayload.push(subscribePayload)
}
const initPayload = {
payload: {
headers: {
xxxxxxxxxxxxx
},
lazy: true,
},
type: "connection_init",
};
const res = ws.connect(wsUri, initPayload, function (socket) {
socket.on('open', function () {
socket.setInterval(function timeout() {
socket.send(JSON.stringify(initPayload));
socket.send(JSON.stringify(ArraySubscribePayload[0]));
socket.setTimeout(function timeout() {
socket.send(JSON.stringify(ArraySubscribePayload[1]));
socket.send(JSON.stringify(ArraySubscribePayload[2]));
ArraySubscribePayload[1].id = String(parseInt(ArraySubscribePayload[1].id) + 1)
ArraySubscribePayload[2].id = String(parseInt(ArraySubscribePayload[2].id) + 1)
}, 3000);
ArraySubscribePayload[0].id = String(parseInt(ArraySubscribePayload[0].id) + 1)
socket.ping();
}, 7000);
})
});
}

RXJS split by id and process in sequence for each id

Problem: Game: So I have some ships that can arrive to many planets. If the 2 ships arrive at the same time on the new planet can lead to the same process of changing ownership twice. This process is asynchronous and should only happen once per planet ownership change.
To fix this I want split the stream of ships by planet id so each stream will be for only one planet. Now the tricky part is that each ship should only be processed after the previous one has been processed.
Ships$
Split by planet id
planet id1: process in sequence
planet id2: process in sequence
...
Here is some code that will show how it should behave.
const ships = [
{
id: 1,
planetId: 1,
},
{
id: 2,
planetId: 1,
},
{
id: 3,
planetId: 2,
},
// ... never finishes
]
// the source observable never finishes
const source$ = interval(1000).pipe(
take(ships.length),
map(i => ships[i]),
)
const createSubject = (ship) => {
// Doesn't need to be a subject, but needs to emit new items after a bit of time based on some other requests.
console.log(`>>>`, ship.id);
const subject = new Subject();
setTimeout(() => {
subject.next(ship.id + ' a' + new Date());
}, 1000);
setTimeout(() => {
subject.next(ship.id + ' b' + new Date());
subject.complete();
}, 2000);
return subject.asObservable();
}
// The result should be the following (t, is the time in seconds, t3, is time after 3 seconds)
// t0: >>> 1
// t0: >>> 3
// t1: 1 a
// t1: 2 a
// t2: 1 b
// t2: 2 b
// t2: >>> 2 (note that the second ship didn't call the createSubject until the first finished)
// t3: 1 a
// t4: 1 2
Solution (with a lot of help from A.Winnen and some figuring out)
Run it here: https://stackblitz.com/edit/angular-8zopfk?file=src/app/app.component.ts
const ships = [
{
id: 1,
planetId: 1,
},
{
id: 2,
planetId: 1,
},
{
id: 3,
planetId: 2,
}
];
const createSubject = (ship) => {
console.log(ship.id + ' a')
const subject = new Subject();
setTimeout(() => {
//subject.next(ship.id + ' b');
}, 500);//
setTimeout(() => {
subject.next(ship.id + ' c');
subject.complete();//
}, 1000);
return subject.asObservable();
}
let x = 0;
interval(10).pipe(//
take(ships.length),
map(i => ships[i]),
groupBy(s => s.planetId),
mergeMap(group$ => {//
x++
return group$.pipe(
tap(i => console.log('x', i, x)),
concatMap(createSubject)
)
}),
).subscribe(res => console.log('finish', res), undefined, () => console.log("completed"))
How can this be done in rxjs?
Code:
const shipArriveAction$ = action$.pipe<AppAction>(
ofType(ShipActions.arrive),
groupBy(action => action.payload.ship.toPlanetId),
mergeMap((shipByPlanet$: Observable<ShipActions.Arrive>) => {
return shipByPlanet$.pipe(
groupBy(action => action.payload.ship.id),
mergeMap((planet$) => {
return planet$.pipe(
concatMap((action) => {
console.log(`>>>concat`, new Date(), action);
// this code should be called in sequence for each ship with the same planet. I don't need only the results to be in order, but also this to be called in sequence.
const subject = new Subject();
const pushAction: PushAction = (pushedAction) => {
subject.next(pushedAction);
};
onShipArriveAction(state$.value, action, pushAction).then(() => {
subject.complete();
});
return subject.asObservable();
}),
)
})
);
)
;
The code from A.Winnen is very close, but only works with a source observable that is finished, not continuous:
const ships = [
{
id: 1,
planetId: 1,
},
{
id: 2,
planetId: 1,
},
{
id: 3,
planetId: 2,
}
];
const createSubject = (ship) => {
console.log(ship.id + ' a')
const subject = new Subject();
setTimeout(() => {
subject.next(ship.id + ' b');
}, 1000);//
setTimeout(() => {
subject.next(ship.id + ' c');
subject.complete();//
}, 2000);
return subject.asObservable().pipe(
finalize(null)
);
}
interval(1000).pipe(
take(ships.length),
tap(console.log),
map(i => ships[i]),
groupBy(s => s.planetId),
mergeMap(group => group.pipe(toArray())),
mergeMap(group => from(group).pipe(
concatMap(createSubject)
))
).subscribe(res => console.log(res), undefined, () => console.log("completed"))
you can use a combination of groupBy and mergeMap to achieve your goal.
from(ships).pipe(
groupBy(ship => ship.planetId),
mergeMap(planetGroup => planetGroup.pipe(
concatMap(ship => {
// do real processing in this step
return of(`planetGroup: ${planetGroup.key} - processed ${ship.ship}`);
})
))
).subscribe(result => console.log(result));
I made a simple example: https://stackblitz.com/edit/angular-6etaja?file=src%2Fapp%2Fapp.component.ts
EDIT:
updated blitzstack: https://stackblitz.com/edit/angular-y7znvk

Apply delta values on nested fields

Suppose I have record like this:
{
id: 1,
statistics: {
stat1: 1,
global: {
stat2: 3
},
stat111: 99
}
}
I want to make update on record with object:
{
statistics: {
stat1: 8,
global: {
stat2: 6
},
stat4: 3
}
}
And it should be added to current record as delta. So, the result record should looks like this:
{
id: 1,
statistics: {
stat1: 9,
global: {
stat2: 9
},
stat4: 3,
stat111: 99
}
}
Is it possible to make this with one query?
Do you want something generic or something specific?
Specific is easy, this is the generic case:
const updateValExpr = r.expr(updateVal);
const updateStats = (stats, val) => val
.keys()
.map(key => r.branch(
stats.hasFields(key),
[key, stats(key).add(val(key))],
[key, val(key)]
))
.coerceTo('object')
r.table(...)
.update(stats =>
updateStats(stats.without('global'), updateValExpr.without('global'))
.merge({ global: updateStats(stats('global'), updateValExpr('global'))
)
There might be some bugs here sincce it's untested but the solution key point is the updateStats function, the fact that you can get all the keys with .keys() and that coerceTo('object') transforms this array: [['a',1],['b',2]] to this object: { a: 1, b: 2 },
Edit:
You can do it recursively, although with limited stack (since you can't send recursive stacks directly, they resolve when the query is actually built:
function updateStats(stats, val, stack = 10) {
return stack === 0
? {}
: val
.keys()
.map(key => r.branch(
stats.hasFields(key).not(),
[key, val(key)],
stats(key).typeOf().eq('OBJECT'),
[key, updateStats(stats(key), val(key), stack - 1)],
[key, stats(key).add(val(key))]
)).coerceTo('object')
}
r.table(...).update(row => updateStats(row, r(updateVal)).run(conn)
// test in admin panel
updateStats(r({
id: 1,
statistics: {
stat1: 1,
global: {
stat2: 3
},
stat111: 99
}
}), r({
statistics: {
stat1: 8,
global: {
stat2: 6
},
stat4: 3
}
}))

Resources