Skip to content

Commit

Permalink
Merge f8e43b4 into 35857d7
Browse files Browse the repository at this point in the history
  • Loading branch information
kiramclean committed Apr 5, 2019
2 parents 35857d7 + f8e43b4 commit 2270e61
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 0 deletions.
100 changes: 100 additions & 0 deletions projects/s-rxjs-utils/src/lib/map-with-caching.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Subject } from "rxjs";
import { mapArrayWithCaching } from "./map-with-caching";

describe("mapArrayWithCaching()", () => {
it("maps over the array using the given function", () => {
const source = new Subject<number[]>();
let lastEmittedThing: number[] = [];

source
.asObservable()
.pipe(mapArrayWithCaching((item) => item.toString(), (item) => item * 3))
.subscribe((value) => {
lastEmittedThing = value;
});

const array = [1, 2, 3, 4, 5, 6];
source.next(array);
expect(lastEmittedThing).toEqual([3, 6, 9, 12, 15, 18]);

array.splice(2, 2); // array = [1, 2, 5, 6]
source.next(array);
expect(lastEmittedThing).toEqual([3, 6, 15, 18]);

array.push(10); // array = [1, 2, 5, 6, 10]
source.next(array);
expect(lastEmittedThing).toEqual([3, 6, 15, 18, 30]);
});

it("emits the same object reference for items that have the same cache key", async () => {
const source = new Subject<Array<{ index: number }>>();
let lastEmittedThing: Array<{ index: number }> = [];

source
.asObservable()
.pipe(
mapArrayWithCaching(
(item) => item.index.toString(),
(item) => ({ index: item.index + 1 }),
),
)
.subscribe((value) => {
lastEmittedThing = value;
});

source.next([{ index: 1 }]);
expect(lastEmittedThing).toEqual([{ index: 2 }]);
const mappedItem1 = lastEmittedThing[0];

source.next([{ index: 1 }, { index: 2 }]);
expect(lastEmittedThing).toEqual([{ index: 2 }, { index: 3 }]);
expect(lastEmittedThing[0]).toBe(mappedItem1);
});

it("handles the upstream source completing", () => {
const source = new Subject<number[]>();
const complete = jasmine.createSpy();

source
.pipe(mapArrayWithCaching((item) => item.toString(), (item) => item + 1))
.subscribe(undefined, undefined, complete);

source.complete();

expect(source.observers.length).toBe(0);
expect(complete).toHaveBeenCalledTimes(1);
});

it("handles errors", () => {
const source = new Subject<number[]>();
const error = jasmine.createSpy();

source
.pipe(mapArrayWithCaching((item) => item.toString(), (item) => item + 1))
.subscribe(undefined, error);

source.error("fire!!");

expect(source.observers.length).toBe(0);
expect(error).toHaveBeenCalledTimes(1);
expect(error).toHaveBeenCalledWith("fire!!");
});

it("handles unsubscribes", () => {
const source = new Subject<number[]>();

const subscription1 = source
.pipe(mapArrayWithCaching((item) => item.toString(), (item) => item + 1))
.subscribe();
const subscription2 = source
.pipe(mapArrayWithCaching((item) => item.toString(), (item) => item + 1))
.subscribe();
expect(source.observers.length).toBe(2);

subscription1.unsubscribe();
expect(source.observers.length).toBe(1);

subscription2.unsubscribe();
expect(source.observers.length).toBe(0);
});
});
46 changes: 46 additions & 0 deletions projects/s-rxjs-utils/src/lib/map-with-caching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ObjectWith } from "micro-dash";
import { map } from "rxjs/operators";

/**
* Applies a given function to each item in the upstream array and emits the result. Each item is then cached using the key generated by `buildCacheKey` so that the next emission contains references to the matching objects from previous emission, without running `buildDownstreamItem` again. The cache is only held between successive emissions.
*
* @param buildCacheKey A function that converts an upstream object into a string to use as the cache key. Needs to return a unique key for each item in the source array.
* @param buildDownstreamItem A function that converts an upstream object into a downstream object
*
* ```
* const mapWithCaching = mapArrayWithCaching(
* (item) => item.toString(),
* (item) => item + 1
* )
*
* source: -[1, 2]---[1, 2, 3]---[2]--|
* mapWithCaching: -[2, 3]---[2, 3, 4]---[3]--|
* ```
*/
export function mapArrayWithCaching<U, D>(
buildCacheKey: (upstreamItem: U) => string,
buildDownstreamItem: (upstreamItem: U) => D,
) {
let cache: ObjectWith<D> = {};

return map((upstreamItems: U[]) => {
const nextCache: ObjectWith<D> = {};

const downstreamItems = upstreamItems.map((upstreamItem) => {
const cacheKey = buildCacheKey(upstreamItem);

let downstreamItem: D;
if (cache.hasOwnProperty(cacheKey)) {
downstreamItem = cache[cacheKey];
} else {
downstreamItem = buildDownstreamItem(upstreamItem);
}

nextCache[cacheKey] = downstreamItem;
return downstreamItem;
});

cache = nextCache;
return downstreamItems;
});
}

0 comments on commit 2270e61

Please sign in to comment.