Skip to content

Commit

Permalink
Merge 171f80c into 35857d7
Browse files Browse the repository at this point in the history
  • Loading branch information
kiramclean committed Apr 3, 2019
2 parents 35857d7 + 171f80c commit 67ad80d
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 value in the array emitted by the source Observable, and emits the resulting array as an Observable. Each item in the array is cached using key generated by the given function so that the resulting arrays contain references to the exact same objects included in the previous emission, when possible.
*
* @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 67ad80d

Please sign in to comment.