Is there a way to cancel/remove individual observables in a flatMap or concatMap operation before they complete?
I currently have an observable of UI actions that trigger API requests, and I use flatMap for this:
actionsFromUI.flatMap(a => http.request("blah?id=" + a.id))
This is then subscribed to render results. A really nice property of this is that if I unsubscribe, all in-progress API requests are automatically cancelled. I don't want to break that.
Now I want to support UI actions that can cancel a particular in-progress request. A UI action can trigger an API request for a particular ID, and then a subsequent UI action can then cancel the API request for that ID.
I hope this dummy code illustrates what I'm trying to do:
let actions = [
{ action: "start", id: 1 },
{ action: "start", id: 2 },
{ action: "start", id: 3 },
{ action: "cancel", id: 1 },
{ action: "start", id: 4 },
{ action: "cancel", id: 3 },
{ action: "start", id: 1 }
];
Observable.from(actions)
.flatMap(a => {
if (a.action === "start") {
return http.request("blah?id=" + a.id);
} else if (a.action === "cancel") {
// ???
}
})
.subscribe(response => {
console.log(response);
});
Yes there is way to do that.
You can use takeUntil operator for that.
It would be some think like this:
// actions - observable of user actions, each action has id field
// cancelActions - observable of action cancelations each has id of action to cancel
// doRequest - function that starts request and returns observable
const results = actions.flatMap(
action => doRequest(action)
.takeUntil(
cancelActions.filter(cancel => cancel.id === action.id)
)
);
https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/takeuntil.md
Here's a way to do this.
Start with an observable of the raw data:
let actions = Observable.from([
{ action: "start", id: 1 },
{ action: "start", id: 2 },
{ action: "start", id: 3 },
{ action: "cancel", id: 1 },
{ action: "start", id: 4 },
{ action: "cancel", id: 3 },
{ action: "start", id: 1 }
]);
I then created two functions to simulate the processing and cancelling of the data:
let start = n =>
Observable
.of(`Finished ${n}`)
.delay(1000);
let cancel = n =>
Observable
.of(`Cancelled ${n}`);
I made start take 1 second to process and cancel return immediately.
Then I ran this query:
let query =
actions
.groupBy(x => x.id)
.map(gx =>
gx
.map(x => x.action === "start"
? start(x.id)
: cancel(x.id))
.switch())
.mergeAll();
That gave me this result:
Cancelled 1
Cancelled 3
Finished 2
Finished 4
Finished 1
I think that behaves the way you want it.
I'd appreciate if someone could translate to JS for me.
Related
For each product we do the following:
When a new product is added, create a new Product with POST /v1/catalogs/products
Using the product_id from step 1, create a new plan POST /v1/billing/plans
Whenever a customer clicks "Subscribe" button, we create a new subscription using plan_id from step 2 with POST /v1/billing/subscriptions
Problem: When creating the subscription we are able to change the price the customer will be billed by passing the plan object to POST /v1/billing/subscriptions endpoint to override the amount of the plan. However passing in a different currency throws an error:
"The currency code is different from the plan's currency code."
With that being said, is thereĀ a way to setup paypal subscriptions where we can pass in a different currency? Is it required to create a new plan for each currency because this does not seem like a good solution
We create a billing plan with the following body:
{
product_id: productId,
name: planName,
status: 'ACTIVE',
billing_cycles: [
{
frequency: {
interval_unit: 'MONTH',
interval_count: 1
},
tenure_type: 'REGULAR',
sequence: 1,
// Create a temporary pricing_scheme. This will be replaced
// with a variable amount when a subscription is created.
pricing_scheme: {
fixed_price: {
value: '1',
currency_code: 'CAD'
}
},
total_cycles: 0
}
],
payment_preferences: {
auto_bill_outstanding: true,
payment_failure_threshold: 2
}
}
We create subscription with the following body (however passing in a different currency than the plan (CAD) throws an error):
{
plan_id: planId,
subscriber: {
email_address: email
},
plan: {
billing_cycles: [
{
sequence: 1,
pricing_scheme: {
fixed_price: {
value: amount,
currency_code: currency
}
}
}
]
},
custom_id: productId
};
Is it required to create a new plan for each currency
Yes
I want to help the user author handlebars/mustache templates so when they type a { character, an autocomplete of known template values comes up to assist the user. But the user may not want to choose one of the suggested options, they might just want to continue typing a new template value and terminate it with a }. In the example in the code below, the options "Order Number" and "Delivery Date" are the known autocomplete options, but I want the user to be able to type {Invoice Number} for example.
I've succeeded in make this work in one way. As a user is typing {Invoice Number} I add it to the list of allowed options in the autocompleter fetch function. So the user can get what they want into the document if the click on the suggested option. Clicking fires the onAction function. I want onAction to fire as soon as the user types the closing handlebars character i.e. }.
Here is the code I have tried. I am using TinyMCE 5.
let template = (function () {
'use strict';
tinymce.PluginManager.add("template", function (editor, url) {
const properties = [
{value: "Order Number", text: "Order Number"},
{value: "Delivery Date", text: "Delivery Date"}
];
const insertNewProperty = function(value) {
let property = {value: value, text: value};
properties.push(property);
return property;
};
editor.ui.registry.addAutocompleter('autocompleter-template', {
ch: '{',
minChars: 0,
columns: 1,
fetch: function (pattern) {
return new tinymce.util.Promise(function (resolve) {
let filteredProperties = pattern ? properties.filter(p => p.text.indexOf(pattern) > -1) : properties;
if (filteredProperties.length > 0) {
resolve(filteredProperties);
} else {
resolve([{value: pattern, text: pattern}]);
}
});
},
onAction: function (autocompleteApi, rng, value) {
let property = properties.find(p => p.value === value);
if (!property) {
property = insertNewProperty(value)
}
let content = `{${property.text}}`;
editor.selection.setRng(rng);
editor.insertContent(content);
autocompleteApi.hide();
}
});
return {
getMetadata: function () {
return {
name: "Learning",
url: "https://stackoverflow.com"
};
}
};
});
}());
})();```
You can add a matches function to your callbacks, along with your onAction, this help you do it.
matches: function (rng, text, pattern) {
if(pattern.endsWith('}')){
pattern = pattern.replace('}', '');
/**
* Check if pattern does not match an existing
* variable and do what you want
*/
return true;
}
},
And make sure the function always returns true.
I have a Cloud Functions transaction that uses FieldValue.increment() to update a nested map, but it isn't running atomically, thus the value updates aren't accurate (running the transaction in quick succession results in an incorrect count).
The function is fired via:
export const updateCategoryAndSendMessage= functions.firestore.document('events/{any}').onUpdate((event, context) => {
which include the following transaction:
db.runTransaction(tx => {
const categoryCounterRef = db.collection("data").doc("categoryCount")
const intToIncrement = event.after.data().published == true ? 1 : -1;
const location = event.after.data().location;
await tx.get(categoryCounterRef).then(doc => {
for (const key in event.after.data().category) {
event.after.data().category[key].forEach(async (subCategory) => {
const map = { [key]: { [subCategory]: FieldValue.increment(intToIncrement) } };
await tx.set(categoryCounterRef, { [location]: map }, { merge: true })
})
}
},
).then(result => {
console.info('Transaction success!')
})
.catch(err => {
console.error('Transaction failure:', err)
})
}).catch((error) => console.log(error));
Example:
Value of field to increment: 0
Tap on button that performs the function multiple times in quick succession (to switch between true and false for "Published")
Expected value: 0 or 1 (depending on whether reference document value is true or false)
Actual value: -3, 5, -2 etc.
As far as I'm aware, transactions should be performed "first come, first served" to avoid inaccurate data. It seems like the function isn't "queuing up" correctly - for lack of a better word.
I'm a bit stumped, would greatly appreciate any guidance with this.
Oh goodness, I was missing return...
return db.runTransaction(tx => {
Below is my publish function which is pretty much a copy from Average Aggregation Queries in Meteor
What it's used for: display a list of documents. The user can then click on one item (restaurant) and can look at the details. Within the details there are arrows previous and next which would allow to cicle through the list.
My idea was to use that aggregation method to enrich every document with the previous and next document id, that way I could easily build my previous and next links.
But how? I have no idea. Maybe there is a better way?
Meteor.publish("restaurants", function(coords, options) {
if (coords == undefined) {
return false;
}
if (options == undefined) {
options = {};
}
if (options.num == undefined) {
options.num = 4
}
console.log(options)
/**
* the following code is from https://stackoverflow.com/questions/18520567/average-aggregation-queries-in-meteor
* Awesome piece!!!
*
* */
var self = this;
// This works for Meteor 1.0
var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
// Your arguments to Mongo's aggregation. Make these however you want.
var pipeline = [
{
$geoNear: {
near: { type: "Point", coordinates: [ coords.lng , coords.lat ] },
distanceField: "dist.calculated",
includeLocs: "dist.location",
num: options.num,
spherical: true
}
}
];
db.collection("restaurants").aggregate(
pipeline,
// Need to wrap the callback so it gets called in a Fiber.
Meteor.bindEnvironment(
function(err, result) {
// Add each of the results to the subscription.
_.each(result, function(e) {
e.dist.calculated = Math.round(e.dist.calculated/3959/0.62137*10)/10;
self.added("restaurants", e._id, e);
});
self.ready();
},
function(error) {
Meteor._debug( "Error doing aggregation: " + error);
}
)
);
});
If you don't have a lot of documents on result, you can do that with JavaScript. Change your subscription code to something like this (I didn't test this code).
_.each(result, function(e, idx) {
if(result[idx - 1]) e.prev = result[idx - 1]._id;
if(result[idx + 1]) e.next = result[idx + 1]._id;
e.dist.calculated = Math.round(e.dist.calculated/3959/0.62137*10)/10;
self.added("restaurants", e._id, e);
});
I am trying to block communications in my XMPP client (Built on top of strophe.js). The problem is it only blocks my messages to a contact I am trying to "mute" BUT doesn't block any incoming messages from that contact.
Here is the logic (based on http://xmpp.org/rfcs/rfc3921.html#privacy):
1) Add "bill#domain.me" to my "block" list
var recipient = "bill#domain.me"
var block = $iq({type: 'set'}).c('query', {xmlns: 'jabber:iq:privacy'}).
c('list', {name: 'block'}).
c('item', {type: 'jid', value: recipient, action: 'deny', order: 1}).
c('message');
2) Make this list active
var setListActive = $iq({type: 'set'}).c('query', {xmlns: 'jabber:iq:privacy'}).c("active", {name: "block"});
SGN.connection.sendIQ(setListActive);
What could be the problem?
I might be wrong but what I've understood is it is the way it should work.
If you check the list you were adding jids to you'll see that they are all there:
var getMyPrivacyList = $iq({type: 'get'}).c('query', {xmlns: 'jabber:iq:privacy'}).c("list", {name: "block"});
APP.connection.sendIQ(getMyPrivacyList,function success(response) { console.log(response) });
However if you want to block incoming messages you'd have to manually check senders's jids against this list every time message comes in.
you have to send unsubscribe presence to that buddy as well. and have to put an handler to handle presence type="unsubscribed". on that handler, send unsubscribe presence.
code for blocking friend
const blockFriend_Stanza=function(fromJid, friendJid,cb_success){
connection.send($pres({ to: friendJid, type: "unavailable", }));
connection.send($pres({ to: friendJid, type: "unsubscribe" }));
let UnblockIq=$iq({ 'type': 'set', 'id': 'blockFriend', from: fromJid })
.c("block", {xmlns: "urn:xmpp:blocking"}).c("item", {jid: friendJid});
connection.sendIQ(UnblockIq,
response=>{
console.log("onClick_lnkBlockFriend",response)
if(response.getAttribute("type")=="result"){
cb_success();
}
},
error=>{
console.log("onClick_lnkBlockFriend Error",error)
}
);
let updateRosterIq=$iq({ 'type': 'set', 'id': 'acceptedReq' })
.c("query", {xmlns: Strophe.NS.ROSTER}).c("item", {jid: friendJid,ask:null, subscription: "remove"});
connection.sendIQ(updateRosterIq,response=>{ console.log("onClick_lnkBlockFriend_rosterRemoval",response)},error=>{ console.log("onClick_lnkBlockFriend_rosterRemoval Error",error)});
}
Code for unsubscribe handler
function onPresence(presence) {
console.log("presense obj",presence);
var presence_type = $(presence).attr('type'); // unavailable, subscribed, etc...
var from = $(presence).attr('from'); // presence coming from jabber_id
var to = $(presence).attr('to'); // presence coming to jabber_id
if(presence_type == 'unsubscribe')
{
connection.send($pres({ to: from, type: "unsubscribed" }));
connection.send($pres({ to: from, type: "unavailable", }));
}
return true;
}