How do I memoize the last result in a chain of observables?












1















I've modelled a navigation concept reactively, (a user navigating forward and backward pages) but I'm having trouble optimizing it using Reactive Extensions. I'm sure it's some kind of buffering or sharing operator, but I'm not quite sure which one it is or where to shove it.



This is a working, simplified example of what I've implemented, in TypeScript and with rxjs.



Each time the user clicks, we increment it by one step. (In reality, there is a network request in here which might return a new step or none, hence why this is not a simple + 1.)



describe('navigate', () => {
it('should increment', () => {
const initial = Observable.of(0);
const plusOne = (ns: Observable<number>) => ns.pipe(map(n => n + 1), tap(x => console.log("Computed", x)));
const clicks = Observable.from([plusOne, plusOne, plusOne]);
const expected = cold("(wxyz|)", { w: 0, x: 1, y: 2, z: 3 });

const result = clicks.pipe(
scan(
(s: Observable<number>, x: (ns: Observable<number>) => Observable<number>) => x(s),
initial),
startWith(initial),
switchAll(),
)

expect(result).toBeObservable(expected);
})
})


It produces the correct output, and the test passes.



But in terms of execution, in the console you'll see this printed:



Computed 1
Computed 1
Computed 2
Computed 1
Computed 2
Computed 3


which makes sense, but if the plusOne operation is expensive (e.g. a network request) it won't do to have the plusOnes computed from the start every time.



How do I memoize the result of the last plusOne so that subsequent plusOnes need not calculate the entire chain again?
Or if I'm looking at this the wrong way, how should I be approaching this problem?



Attempt



Since the top of this question, I did think of one solution.



const result = clicks.pipe(
scan(
(s: Observable<number>, x: (ns: Observable<number>) => Observable<number>) => x(s).pipe(shareReplay(1)),
initial),
startWith(initial),
switchAll(),
)


which executes like:



Computed 1
Computed 2
Computed 3


However I think this leads to a chain which would look like:



xs.map(x => x + 1).shareReplay(1).map(x => x + 1).shareReplay(1).map(x => x + 1).shareReplay(1)


and which I don't think would be terribly efficient (if each shareReplay(1) caches a value). Is there a better way to do this?










share|improve this question




















  • 1





    You only need one shareReplay(1) at the end no? or use a behavior subject to just return the last valid result.

    – Fan Cheung
    Jan 2 at 6:12













  • Why are you passing Observables instead of values? Wouldnt it be easier if you operated on number instead of Observable<number>?

    – Davy
    Jan 2 at 9:40













  • I think i would handle the api calls inside of a concatMap. What is executed inside of a concatMap is guaranteed to execute sequentially. Inside of that concatMap you could use withLatestFrom to return the result of the last executed api call, and in ther last step inside that concatMap i would update the last executed result (in either a state store, or simply a subject). So you would just get the last response, get a new response, update last response in a sequential way.

    – Davy
    Jan 2 at 9:44













  • @FanCheung, if you're referring to the chain, that is just a representation of the observable that would be composed at runtime using my attempt. If you're suggesting placing a shareReplay(1) at the end then no, that would be shareReplay'ing for subscribers of 'result'.

    – Nick Darvey
    Jan 4 at 8:43











  • @Davy RE: "Why are you passing Observables instead of values?". It's because: "In reality, there is a network request in here which might return a new step or none, hence why this is not a simple + 1."

    – Nick Darvey
    Jan 4 at 8:48


















1















I've modelled a navigation concept reactively, (a user navigating forward and backward pages) but I'm having trouble optimizing it using Reactive Extensions. I'm sure it's some kind of buffering or sharing operator, but I'm not quite sure which one it is or where to shove it.



This is a working, simplified example of what I've implemented, in TypeScript and with rxjs.



Each time the user clicks, we increment it by one step. (In reality, there is a network request in here which might return a new step or none, hence why this is not a simple + 1.)



describe('navigate', () => {
it('should increment', () => {
const initial = Observable.of(0);
const plusOne = (ns: Observable<number>) => ns.pipe(map(n => n + 1), tap(x => console.log("Computed", x)));
const clicks = Observable.from([plusOne, plusOne, plusOne]);
const expected = cold("(wxyz|)", { w: 0, x: 1, y: 2, z: 3 });

const result = clicks.pipe(
scan(
(s: Observable<number>, x: (ns: Observable<number>) => Observable<number>) => x(s),
initial),
startWith(initial),
switchAll(),
)

expect(result).toBeObservable(expected);
})
})


It produces the correct output, and the test passes.



But in terms of execution, in the console you'll see this printed:



Computed 1
Computed 1
Computed 2
Computed 1
Computed 2
Computed 3


which makes sense, but if the plusOne operation is expensive (e.g. a network request) it won't do to have the plusOnes computed from the start every time.



How do I memoize the result of the last plusOne so that subsequent plusOnes need not calculate the entire chain again?
Or if I'm looking at this the wrong way, how should I be approaching this problem?



Attempt



Since the top of this question, I did think of one solution.



const result = clicks.pipe(
scan(
(s: Observable<number>, x: (ns: Observable<number>) => Observable<number>) => x(s).pipe(shareReplay(1)),
initial),
startWith(initial),
switchAll(),
)


which executes like:



Computed 1
Computed 2
Computed 3


However I think this leads to a chain which would look like:



xs.map(x => x + 1).shareReplay(1).map(x => x + 1).shareReplay(1).map(x => x + 1).shareReplay(1)


and which I don't think would be terribly efficient (if each shareReplay(1) caches a value). Is there a better way to do this?










share|improve this question




















  • 1





    You only need one shareReplay(1) at the end no? or use a behavior subject to just return the last valid result.

    – Fan Cheung
    Jan 2 at 6:12













  • Why are you passing Observables instead of values? Wouldnt it be easier if you operated on number instead of Observable<number>?

    – Davy
    Jan 2 at 9:40













  • I think i would handle the api calls inside of a concatMap. What is executed inside of a concatMap is guaranteed to execute sequentially. Inside of that concatMap you could use withLatestFrom to return the result of the last executed api call, and in ther last step inside that concatMap i would update the last executed result (in either a state store, or simply a subject). So you would just get the last response, get a new response, update last response in a sequential way.

    – Davy
    Jan 2 at 9:44













  • @FanCheung, if you're referring to the chain, that is just a representation of the observable that would be composed at runtime using my attempt. If you're suggesting placing a shareReplay(1) at the end then no, that would be shareReplay'ing for subscribers of 'result'.

    – Nick Darvey
    Jan 4 at 8:43











  • @Davy RE: "Why are you passing Observables instead of values?". It's because: "In reality, there is a network request in here which might return a new step or none, hence why this is not a simple + 1."

    – Nick Darvey
    Jan 4 at 8:48
















1












1








1


1






I've modelled a navigation concept reactively, (a user navigating forward and backward pages) but I'm having trouble optimizing it using Reactive Extensions. I'm sure it's some kind of buffering or sharing operator, but I'm not quite sure which one it is or where to shove it.



This is a working, simplified example of what I've implemented, in TypeScript and with rxjs.



Each time the user clicks, we increment it by one step. (In reality, there is a network request in here which might return a new step or none, hence why this is not a simple + 1.)



describe('navigate', () => {
it('should increment', () => {
const initial = Observable.of(0);
const plusOne = (ns: Observable<number>) => ns.pipe(map(n => n + 1), tap(x => console.log("Computed", x)));
const clicks = Observable.from([plusOne, plusOne, plusOne]);
const expected = cold("(wxyz|)", { w: 0, x: 1, y: 2, z: 3 });

const result = clicks.pipe(
scan(
(s: Observable<number>, x: (ns: Observable<number>) => Observable<number>) => x(s),
initial),
startWith(initial),
switchAll(),
)

expect(result).toBeObservable(expected);
})
})


It produces the correct output, and the test passes.



But in terms of execution, in the console you'll see this printed:



Computed 1
Computed 1
Computed 2
Computed 1
Computed 2
Computed 3


which makes sense, but if the plusOne operation is expensive (e.g. a network request) it won't do to have the plusOnes computed from the start every time.



How do I memoize the result of the last plusOne so that subsequent plusOnes need not calculate the entire chain again?
Or if I'm looking at this the wrong way, how should I be approaching this problem?



Attempt



Since the top of this question, I did think of one solution.



const result = clicks.pipe(
scan(
(s: Observable<number>, x: (ns: Observable<number>) => Observable<number>) => x(s).pipe(shareReplay(1)),
initial),
startWith(initial),
switchAll(),
)


which executes like:



Computed 1
Computed 2
Computed 3


However I think this leads to a chain which would look like:



xs.map(x => x + 1).shareReplay(1).map(x => x + 1).shareReplay(1).map(x => x + 1).shareReplay(1)


and which I don't think would be terribly efficient (if each shareReplay(1) caches a value). Is there a better way to do this?










share|improve this question
















I've modelled a navigation concept reactively, (a user navigating forward and backward pages) but I'm having trouble optimizing it using Reactive Extensions. I'm sure it's some kind of buffering or sharing operator, but I'm not quite sure which one it is or where to shove it.



This is a working, simplified example of what I've implemented, in TypeScript and with rxjs.



Each time the user clicks, we increment it by one step. (In reality, there is a network request in here which might return a new step or none, hence why this is not a simple + 1.)



describe('navigate', () => {
it('should increment', () => {
const initial = Observable.of(0);
const plusOne = (ns: Observable<number>) => ns.pipe(map(n => n + 1), tap(x => console.log("Computed", x)));
const clicks = Observable.from([plusOne, plusOne, plusOne]);
const expected = cold("(wxyz|)", { w: 0, x: 1, y: 2, z: 3 });

const result = clicks.pipe(
scan(
(s: Observable<number>, x: (ns: Observable<number>) => Observable<number>) => x(s),
initial),
startWith(initial),
switchAll(),
)

expect(result).toBeObservable(expected);
})
})


It produces the correct output, and the test passes.



But in terms of execution, in the console you'll see this printed:



Computed 1
Computed 1
Computed 2
Computed 1
Computed 2
Computed 3


which makes sense, but if the plusOne operation is expensive (e.g. a network request) it won't do to have the plusOnes computed from the start every time.



How do I memoize the result of the last plusOne so that subsequent plusOnes need not calculate the entire chain again?
Or if I'm looking at this the wrong way, how should I be approaching this problem?



Attempt



Since the top of this question, I did think of one solution.



const result = clicks.pipe(
scan(
(s: Observable<number>, x: (ns: Observable<number>) => Observable<number>) => x(s).pipe(shareReplay(1)),
initial),
startWith(initial),
switchAll(),
)


which executes like:



Computed 1
Computed 2
Computed 3


However I think this leads to a chain which would look like:



xs.map(x => x + 1).shareReplay(1).map(x => x + 1).shareReplay(1).map(x => x + 1).shareReplay(1)


and which I don't think would be terribly efficient (if each shareReplay(1) caches a value). Is there a better way to do this?







rxjs reactive-programming system.reactive






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Jan 2 at 7:09







Nick Darvey

















asked Jan 2 at 6:05









Nick DarveyNick Darvey

126112




126112








  • 1





    You only need one shareReplay(1) at the end no? or use a behavior subject to just return the last valid result.

    – Fan Cheung
    Jan 2 at 6:12













  • Why are you passing Observables instead of values? Wouldnt it be easier if you operated on number instead of Observable<number>?

    – Davy
    Jan 2 at 9:40













  • I think i would handle the api calls inside of a concatMap. What is executed inside of a concatMap is guaranteed to execute sequentially. Inside of that concatMap you could use withLatestFrom to return the result of the last executed api call, and in ther last step inside that concatMap i would update the last executed result (in either a state store, or simply a subject). So you would just get the last response, get a new response, update last response in a sequential way.

    – Davy
    Jan 2 at 9:44













  • @FanCheung, if you're referring to the chain, that is just a representation of the observable that would be composed at runtime using my attempt. If you're suggesting placing a shareReplay(1) at the end then no, that would be shareReplay'ing for subscribers of 'result'.

    – Nick Darvey
    Jan 4 at 8:43











  • @Davy RE: "Why are you passing Observables instead of values?". It's because: "In reality, there is a network request in here which might return a new step or none, hence why this is not a simple + 1."

    – Nick Darvey
    Jan 4 at 8:48
















  • 1





    You only need one shareReplay(1) at the end no? or use a behavior subject to just return the last valid result.

    – Fan Cheung
    Jan 2 at 6:12













  • Why are you passing Observables instead of values? Wouldnt it be easier if you operated on number instead of Observable<number>?

    – Davy
    Jan 2 at 9:40













  • I think i would handle the api calls inside of a concatMap. What is executed inside of a concatMap is guaranteed to execute sequentially. Inside of that concatMap you could use withLatestFrom to return the result of the last executed api call, and in ther last step inside that concatMap i would update the last executed result (in either a state store, or simply a subject). So you would just get the last response, get a new response, update last response in a sequential way.

    – Davy
    Jan 2 at 9:44













  • @FanCheung, if you're referring to the chain, that is just a representation of the observable that would be composed at runtime using my attempt. If you're suggesting placing a shareReplay(1) at the end then no, that would be shareReplay'ing for subscribers of 'result'.

    – Nick Darvey
    Jan 4 at 8:43











  • @Davy RE: "Why are you passing Observables instead of values?". It's because: "In reality, there is a network request in here which might return a new step or none, hence why this is not a simple + 1."

    – Nick Darvey
    Jan 4 at 8:48










1




1





You only need one shareReplay(1) at the end no? or use a behavior subject to just return the last valid result.

– Fan Cheung
Jan 2 at 6:12







You only need one shareReplay(1) at the end no? or use a behavior subject to just return the last valid result.

– Fan Cheung
Jan 2 at 6:12















Why are you passing Observables instead of values? Wouldnt it be easier if you operated on number instead of Observable<number>?

– Davy
Jan 2 at 9:40







Why are you passing Observables instead of values? Wouldnt it be easier if you operated on number instead of Observable<number>?

– Davy
Jan 2 at 9:40















I think i would handle the api calls inside of a concatMap. What is executed inside of a concatMap is guaranteed to execute sequentially. Inside of that concatMap you could use withLatestFrom to return the result of the last executed api call, and in ther last step inside that concatMap i would update the last executed result (in either a state store, or simply a subject). So you would just get the last response, get a new response, update last response in a sequential way.

– Davy
Jan 2 at 9:44







I think i would handle the api calls inside of a concatMap. What is executed inside of a concatMap is guaranteed to execute sequentially. Inside of that concatMap you could use withLatestFrom to return the result of the last executed api call, and in ther last step inside that concatMap i would update the last executed result (in either a state store, or simply a subject). So you would just get the last response, get a new response, update last response in a sequential way.

– Davy
Jan 2 at 9:44















@FanCheung, if you're referring to the chain, that is just a representation of the observable that would be composed at runtime using my attempt. If you're suggesting placing a shareReplay(1) at the end then no, that would be shareReplay'ing for subscribers of 'result'.

– Nick Darvey
Jan 4 at 8:43





@FanCheung, if you're referring to the chain, that is just a representation of the observable that would be composed at runtime using my attempt. If you're suggesting placing a shareReplay(1) at the end then no, that would be shareReplay'ing for subscribers of 'result'.

– Nick Darvey
Jan 4 at 8:43













@Davy RE: "Why are you passing Observables instead of values?". It's because: "In reality, there is a network request in here which might return a new step or none, hence why this is not a simple + 1."

– Nick Darvey
Jan 4 at 8:48







@Davy RE: "Why are you passing Observables instead of values?". It's because: "In reality, there is a network request in here which might return a new step or none, hence why this is not a simple + 1."

– Nick Darvey
Jan 4 at 8:48














0






active

oldest

votes











Your Answer






StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");

StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});

function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});


}
});














draft saved

draft discarded


















StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54001934%2fhow-do-i-memoize-the-last-result-in-a-chain-of-observables%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown

























0






active

oldest

votes








0






active

oldest

votes









active

oldest

votes






active

oldest

votes
















draft saved

draft discarded




















































Thanks for contributing an answer to Stack Overflow!


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


To learn more, see our tips on writing great answers.




draft saved


draft discarded














StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54001934%2fhow-do-i-memoize-the-last-result-in-a-chain-of-observables%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







Popular posts from this blog

'app-layout' is not a known element: how to share Component with different Modules

android studio warns about leanback feature tag usage required on manifest while using Unity exported app?

WPF add header to Image with URL pettitions [duplicate]