Skip to content

Commit

Permalink
feat: add tap operator
Browse files Browse the repository at this point in the history
  • Loading branch information
david-luna committed May 25, 2024
1 parent 0839a15 commit 75596d0
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 34 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,24 @@ operators do not use utils and I tried to avoid any internal dependency.
package ESM only so `require` would fail to load the lib. The new flag
`--experimental-require-module` can be used to load the lib if your app
is in `commonjs` format. Checkout [the docs](https://nodejs.org/docs/latest/api/modules.html#loading-ecmascript-modules-using-require).
* Add `tap` operator

### [0.4.0]

* Fix barrel files to expose latest operators and factories

### [0.3.0]

* Add mergeMap operator
* Add `mergeMap` operator

### [0.2.0]

* Add withLatestFrom operator
* Add `withLatestFrom` operator

### [0.1.0]

* Add combineLatest factory method
* Add `combineLatest` factory method

### [0.0.3]

* Add fromEventPattern factory method
* Add `fromEventPattern` factory method
28 changes: 0 additions & 28 deletions lib/factories/fromEvent.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,3 @@
// // NOTE: to mimic browser's CustomEvent
// type NodeOrBrowserEvent = Event | Event & { detail: any };
// export interface EventTargetLike<T extends NodeOrBrowserEvent> {
// addEventListener(
// type: string,
// listener: ((e: T) => void) | { handleEvent: (e: T) => void } | null,
// options?: any
// ): void;
// removeEventListener(
// type: string,
// listener: ((e: T) => void) | { handleEvent: (e: T) => void } | null,
// options?: any,
// ): void;
// }

// export interface EventEmitterLike<T extends (...args: any[]) => void> {
// addListener(
// type: string,
// listener: T,
// options?: any
// ): void;
// removeListener(
// type: string,
// listener: T,
// options?: any,
// ): void;
// }

import { createObsevable } from '../observable.js';

/**
Expand Down
2 changes: 0 additions & 2 deletions lib/factories/of.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,5 @@ export function of(...inputs) {
return createObsevable((observer) => {
inputs.forEach((input) => observer.next(input));
observer.complete();
// TODO: remove this
return () => undefined;
});
}
1 change: 1 addition & 0 deletions lib/operators/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './mergeMap.js';
export * from './switchMap.js';
export * from './take.js';
export * from './takeWhile.js';
export * from './tap.js';
export * from './withLatestFrom.js';
28 changes: 28 additions & 0 deletions lib/operators/tap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createObsevable } from '../observable.js';

/**
* @template T
* @param {(value: T) => void} fn
* @returns {import('../observable').OperatorFunction<T,T>}
*/
export function tap(fn) {
return (source) => {
return createObsevable((observer) => {
const sourceSubscription = source.subscribe({
next: (value) => {
fn(value);
observer.next(value);
},
error: (err) => {
observer.error(err);
},
complete: () => {
observer.complete();
},
});
return () => {
return sourceSubscription.unsubscribe();
};
});
};
}
73 changes: 73 additions & 0 deletions test/operators/tap.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import assert from 'node:assert';
import { beforeEach, mock, test } from 'node:test';

import { createMockObservable, createMockObserver } from '../__tools__/index.js';

import { tap } from '../../lib/operators/tap.js';

/** @typedef {import('../__tools__/index.js').MockFunction} MockFunction */

// /** @type {(x:number) => void} */
// const log = (x) => console.log(x); // eslint-disable-line no-console -- used for test purposes
const mockLog = mock.fn();
const toLogged = tap(mockLog);

/** @type {import('../__tools__/index.js').ObservableMock<number>} */
let sourceNumbers;
/** @type {import('../../lib/observable.js').Observable<number>} */
let loggedNumbers;
/** @type {import('../__tools__/index.js').ObserverMock<number>} */
let observerMock;
/** @type {import('../../lib/observable.js').Subscription} */
let subscription;
/** @type {MockFunction} */
let nextMock;
/** @type {MockFunction} */
let errorMock;
/** @type {MockFunction} */
let completeMock;
/** @type {MockFunction} */
let tearDownMock;

beforeEach(() => {
sourceNumbers = createMockObservable();
loggedNumbers = toLogged(sourceNumbers.observable);
observerMock = createMockObserver();
subscription = loggedNumbers.subscribe(observerMock);
nextMock = observerMock.next.mock;
errorMock = observerMock.error.mock;
completeMock = observerMock.complete.mock;
tearDownMock = sourceNumbers.mocks.tearDown.mock;
});

test('tap - should emit the same values', () => {
sourceNumbers.triggers.next(2);
sourceNumbers.triggers.next(3);
sourceNumbers.triggers.next(4);
subscription.unsubscribe();

assert.strictEqual(nextMock.callCount(), 3);
assert.deepStrictEqual(nextMock.calls[0].arguments, [2]);
assert.deepStrictEqual(nextMock.calls[1].arguments, [3]);
assert.deepStrictEqual(nextMock.calls[2].arguments, [4]);
// side effect funciton is called
assert.deepStrictEqual(mockLog.mock.calls[0].arguments, [2]);
assert.deepStrictEqual(mockLog.mock.calls[1].arguments, [3]);
assert.deepStrictEqual(mockLog.mock.calls[2].arguments, [4]);
assert.strictEqual(errorMock.callCount(), 0);
assert.strictEqual(completeMock.callCount(), 0);
assert.strictEqual(tearDownMock.callCount(), 1);
});

test('tap - should error if source observable errors', () => {
sourceNumbers.triggers.next(2);
sourceNumbers.triggers.error(new Error('observer error'));
subscription.unsubscribe();

assert.strictEqual(nextMock.callCount(), 1);
assert.deepStrictEqual(nextMock.calls[0].arguments, [2]);
assert.strictEqual(errorMock.callCount(), 1);
assert.strictEqual(errorMock.calls[0].arguments[0].message, 'observer error');
assert.strictEqual(completeMock.callCount(), 0);
assert.strictEqual(tearDownMock.callCount(), 1);
});
6 changes: 6 additions & 0 deletions types/operators/tap.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @template T
* @param {(value: T) => void} fn
* @returns {import('../observable').OperatorFunction<T,T>}
*/
export function tap<T>(fn: (value: T) => void): import("../observable.js").OperatorFunction<T, T>;

0 comments on commit 75596d0

Please sign in to comment.