How do I memoize the last result in a chain of observables?
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 plusOne
s computed from the start every time.
How do I memoize the result of the last plusOne
so that subsequent plusOne
s 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
|
show 2 more comments
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 plusOne
s computed from the start every time.
How do I memoize the result of the last plusOne
so that subsequent plusOne
s 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
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
|
show 2 more comments
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 plusOne
s computed from the start every time.
How do I memoize the result of the last plusOne
so that subsequent plusOne
s 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
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 plusOne
s computed from the start every time.
How do I memoize the result of the last plusOne
so that subsequent plusOne
s 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
rxjs reactive-programming system.reactive
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
|
show 2 more comments
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
|
show 2 more comments
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
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
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.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
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
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