chore: init repository

This commit is contained in:
Guillaume ARM 2023-05-31 11:13:01 +02:00 committed by Guillaume ARM
commit 697b09b8d4
37 changed files with 6316 additions and 0 deletions

77
.drone.yml Normal file
View File

@ -0,0 +1,77 @@
---
kind: pipeline
type: docker
name: default
trigger:
event:
exclude:
- tag
steps:
- name: install_and_build
image: node:18
commands:
- npm install
- npm run build
- name: lint
image: node:18
commands:
- npm run lint
depends_on:
- install_and_build
- name: prettier
image: node:18
commands:
- npm run prettier:check
depends_on:
- install_and_build
- name: jest-unit-test
image: node:18
commands:
- npm run test
depends_on:
- install_and_build
- name: cucumber-functional-test
image: node:18
commands:
- npm run cucumber:prepare
- npm run cucumber:run
depends_on:
- install_and_build
---
kind: pipeline
type: docker
name: release
depends_on:
- default
trigger:
branch:
- master
- next
steps:
- name: install_and_build
image: node:18
commands:
- npm install
- npm run build
- name: semantic_release
image: node:18
environment:
NPM_TOKEN:
from_secret: npm_token
GITEA_TOKEN:
from_secret: gitea_token
GITEA_URL: https://git.trapcodien.com
commands:
- npm run release
depends_on:
- install_and_build

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
/dist/

3
.eslintrc Normal file
View File

@ -0,0 +1,3 @@
{
"extends": ["@trapcodien/eslint-config"]
}

107
.gitignore vendored Normal file
View File

@ -0,0 +1,107 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Parcel
/.parcel-cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

1
.npmrc Normal file
View File

@ -0,0 +1 @@
@garm:registry=https://git.trapcodien.com/api/packages/garm/npm/

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v18.14

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
/dist/

1
.prettierrc.json Normal file
View File

@ -0,0 +1 @@
"@trapcodien/prettier-config"

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 garm
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
README.md Normal file
View File

@ -0,0 +1 @@
# `rxjs-reboot`

14
cucumber.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
default: [
// `--format-options '{"snippetInterface": "synchronous"}'`,
'--parallel 1',
// '--format progress-bar',
// '--format usage',
// '--format snippets',
'--format summary',
'--publish-quiet',
// '--require-module ts-node/register',
// '--require ./src/features/**/*.ts',
// '--require ./src/features/*.ts',
].join(' '),
};

7
jest.config.js Normal file
View File

@ -0,0 +1,7 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
modulePathIgnorePatterns: ['dist'],
setupFilesAfterEnv: ['jest-extended/all'],
};

59
package.json Normal file
View File

@ -0,0 +1,59 @@
{
"name": "@garm/rxjs-reboot",
"version": "0.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"step1": "npm run test:watch Subscription",
"step2": "npm run test:watch Subscriber",
"step3": "npm run test:watch Observable",
"step4": "npm run test:watch Subject",
"step5": "npm run test:watch map",
"step6": "npm run test:watch scan",
"step7": "npm run test:watch retry",
"step8": "npm run test:watch finalize",
"step9": "npm run test:watch switchMap",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"check:all": "npm run build && npm run lint:all",
"tsc": "tsc --build src",
"tsc:watch": "tsc --build src --watch",
"tsc:check": "tsc --build src --noEmit",
"build": "npm run tsc",
"build:watch": "npm run tsc:watch",
"build:check": "npm run tsc:check",
"rebuild": "npm run clean && npm run build",
"lint": "npx eslint src --ext .ts,.tsx,.json --max-warnings=0",
"lint:all": "npm run lint && npm run prettier:check",
"lint:fix": "npm run lint --fix",
"prettier": "prettier --write .",
"prettier:check": "prettier --check .",
"clean": "rimraf dist tsconfig.tsbuildinfo",
"clean:all": "npm run clean",
"test": "NODE_ENV=test jest --verbose --passWithNoTests",
"test:watch": "NODE_ENV=test jest --verbose --passWithNoTests --watch",
"test:all": "npm run clean:all && npm run build && npm run lint:all && npm run test"
},
"keywords": [],
"author": "Garm",
"license": "MIT",
"devDependencies": {
"@cucumber/cucumber": "8.11.0",
"@trapcodien/eslint-config": "1.0.4",
"@trapcodien/prettier-config": "1.0.4",
"@trapcodien/tsconfig": "1.0.6",
"@types/jest": "29.4.0",
"@types/node": "18.13.0",
"copyfiles": "2.4.1",
"eslint": "8.34.0",
"jest": "29.4.2",
"jest-extended": "3.2.4",
"prettier": "2.8.4",
"ts-jest": "29.0.5",
"ts-node": "10.9.1",
"typescript": "4.9.5"
},
"dependencies": {
"rxjs": "7.8.1"
}
}

33
src/Observable.ts Normal file
View File

@ -0,0 +1,33 @@
/**
* Typings
*/
import { Observer } from './Observer';
import { Subscription, TeardownLogic } from './Subscription';
type SubscribeFunction<T> = (obs: Partial<Observer<T>>) => Subscription;
type SubscriptionFactory<T> = (observer: Observer<T>) => TeardownLogic;
export interface IObservable<T> {
subscribe: SubscribeFunction<T>;
}
export interface ObservableConstructor {
new <T>(subscriptionFactory: SubscriptionFactory<T>): IObservable<T>;
}
/**
* Implementation
*/
export class Observable<T> implements IObservable<T> {
constructor(subscriptionFactory: SubscriptionFactory<T>) {
void subscriptionFactory;
throw new Error('TODO');
}
subscribe(obs: Partial<Observer<T>>): Subscription {
void obs;
throw new Error('TODO');
}
}

9
src/Observer.ts Normal file
View File

@ -0,0 +1,9 @@
export type OnNext<T> = (value: T) => void;
export type OnError = (error: unknown) => void;
export type OnComplete = () => void;
export type Observer<T> = {
next: OnNext<T>;
error: OnError;
complete: OnComplete;
};

39
src/Subject.ts Normal file
View File

@ -0,0 +1,39 @@
import { IObservable } from './Observable';
import { Observer } from './Observer';
import { Subscription } from './Subscription';
/**
* Typings
*/
export interface ISubject<T> extends IObservable<T>, Observer<T> {}
export interface SubjectConstructor {
new <T>(): ISubject<T>;
}
/**
* Implementation
*/
export class Subject<T> implements IObservable<T>, Observer<T> {
public constructor() {}
public next(value: T): void {
void value;
throw new Error('TODO');
}
public error(err: unknown): void {
void err;
throw new Error('TODO');
}
public complete(): void {
throw new Error('TODO');
}
public subscribe(obs: Partial<Observer<T>>): Subscription {
void obs;
throw new Error('TODO');
}
}

39
src/Subscriber.ts Normal file
View File

@ -0,0 +1,39 @@
import { Observer } from './Observer';
import { Subscription, ISubscription } from './Subscription';
/**
* Typings
*/
export interface ISubscriber<T> extends ISubscription, Observer<T> {}
export interface SubscriberConstructor {
new <T>(obs: Observer<T>): ISubscriber<T>;
}
/**
* Implementation
*/
export class Subscriber<T> extends Subscription implements Observer<T> {
public constructor(obs: Partial<Observer<T>>) {
super();
void obs;
throw new Error('TODO');
}
/* Observer implementation */
public next(value: T): void {
void value;
throw new Error('TODO');
}
public error(err: unknown): void {
void err;
throw new Error('TODO');
}
public complete(): void {
throw new Error('TODO');
}
}

45
src/Subscription.ts Normal file
View File

@ -0,0 +1,45 @@
import { Unsubscribable } from 'rxjs/internal/types';
/**
* Typings
*/
type Teardown = () => void;
export interface ISubscription {
readonly closed?: boolean;
unsubscribe: Teardown;
add: (t: TeardownLogic) => void;
remove: (s: TeardownLogic) => void;
}
export interface SubscriptionConstructor {
new (t?: Teardown): Subscription;
}
export type TeardownLogic = Subscription | Unsubscribable | (() => void) | void;
/**
* Implementation
*/
export class Subscription implements ISubscription {
public closed = false;
public constructor(action?: Teardown) {
void action;
throw new Error('TODO');
}
public unsubscribe(): void {
throw new Error('TODO');
}
public add(tl: TeardownLogic): void {
void tl;
throw new Error('TODO');
}
public remove(s: TeardownLogic): void {
void s;
throw new Error('TODO');
}
}

View File

@ -0,0 +1,311 @@
import 'jest-extended';
import * as Rx from 'rxjs';
import { Observable, ObservableConstructor } from '../Observable';
import { Observer } from '../Observer';
const testObservable = (ObservableClass: ObservableConstructor) => () => {
it('[constructor] an error should be emitted on throw', () => {
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
const o$ = new ObservableClass(() => {
throw 'ERROR';
});
const sub = o$.subscribe(obs);
expect(onError).toBeCalledWith('ERROR');
expect(onNext).not.toBeCalled();
expect(onComplete).not.toBeCalled();
expect(sub.closed).toBe(true);
});
it('[constructor] subscription should be closed on error', () => {
const obs: Partial<Observer<unknown>> = { error: jest.fn() };
const o$ = new ObservableClass(() => {
throw 'ERROR';
});
const sub = o$.subscribe(obs);
expect(sub.closed).toBe(true);
});
it('[next]', () => {
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
const o$ = new ObservableClass(observer => {
observer.next(42);
});
const sub = o$.subscribe(obs);
expect(onNext).toBeCalledWith(42);
expect(onNext).toBeCalledTimes(1);
expect(onComplete).not.toBeCalled();
expect(onError).not.toBeCalled();
expect(sub.closed).toBe(false);
});
it('[next/complete]', () => {
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
const o$ = new ObservableClass(observer => {
observer.next(42);
observer.complete();
});
const sub = o$.subscribe(obs);
expect(onNext).toBeCalledWith(42);
expect(onNext).toBeCalledTimes(1);
expect(onComplete).toBeCalledTimes(1);
expect(onError).not.toBeCalled();
expect(sub.closed).toBe(true);
});
it('[next/error]', () => {
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
const o$ = new ObservableClass(observer => {
observer.next(42);
observer.error('dummy error');
});
const sub = o$.subscribe(obs);
expect(onNext).toBeCalledWith(42);
expect(onNext).toBeCalledTimes(1);
expect(onComplete).not.toBeCalled();
expect(onError).toHaveBeenCalledWith('dummy error');
expect(onError).toHaveBeenCalledTimes(1);
expect(sub.closed).toBe(true);
});
it('[next] several values', () => {
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
const o$ = new ObservableClass(observer => {
observer.next(-84);
observer.next(-42);
observer.next(42);
observer.next(84);
});
const sub = o$.subscribe(obs);
expect(onNext).toHaveBeenNthCalledWith(1, -84);
expect(onNext).toHaveBeenNthCalledWith(2, -42);
expect(onNext).toHaveBeenNthCalledWith(3, 42);
expect(onNext).toHaveBeenNthCalledWith(4, 84);
expect(onNext).toBeCalledTimes(4);
expect(onComplete).not.toBeCalled();
expect(onError).not.toBeCalled();
expect(sub.closed).toBe(false);
});
it('[next/complete] several values', () => {
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
const o$ = new ObservableClass(observer => {
observer.next(-84);
observer.next(-42);
observer.next(42);
observer.next(84);
observer.complete();
});
const sub = o$.subscribe(obs);
expect(onNext).toHaveBeenNthCalledWith(1, -84);
expect(onNext).toHaveBeenNthCalledWith(2, -42);
expect(onNext).toHaveBeenNthCalledWith(3, 42);
expect(onNext).toHaveBeenNthCalledWith(4, 84);
expect(onNext).toBeCalledTimes(4);
expect(onComplete).toBeCalledTimes(1);
expect(onError).not.toBeCalled();
expect(sub.closed).toBe(true);
});
it('[next/error] several values', () => {
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
const o$ = new ObservableClass(observer => {
observer.next(-84);
observer.next(-42);
observer.next(42);
observer.next(84);
observer.error('dummy error');
});
const sub = o$.subscribe(obs);
expect(onNext).toHaveBeenNthCalledWith(1, -84);
expect(onNext).toHaveBeenNthCalledWith(2, -42);
expect(onNext).toHaveBeenNthCalledWith(3, 42);
expect(onNext).toHaveBeenNthCalledWith(4, 84);
expect(onNext).toBeCalledTimes(4);
expect(onComplete).not.toBeCalled();
expect(onError).toBeCalledTimes(1);
expect(onError).toBeCalledWith('dummy error');
expect(sub.closed).toBe(true);
});
it('[next] with 2 subscriptions', () => {
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
const o$ = new ObservableClass(observer => {
observer.next(42);
});
const subA = o$.subscribe(obs);
expect(onNext).toBeCalledTimes(1);
const subB = o$.subscribe(obs);
expect(subA.closed).toBe(false);
expect(subB.closed).toBe(false);
expect(onNext).toHaveBeenNthCalledWith(1, 42);
expect(onNext).toHaveBeenNthCalledWith(2, 42);
expect(onNext).toBeCalledTimes(2);
expect(onComplete).not.toBeCalled();
expect(onError).not.toBeCalled();
});
it('[complete] with 2 subscriptions', () => {
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
const o$ = new ObservableClass(observer => {
observer.complete();
});
const subA = o$.subscribe(obs);
expect(subA.closed).toBe(true);
expect(onComplete).toBeCalledTimes(1);
const subB = o$.subscribe(obs);
expect(subB.closed).toBe(true);
expect(onComplete).toBeCalledTimes(2);
expect(onNext).not.toBeCalled();
expect(onError).not.toBeCalled();
});
it('[error] with 2 subscriptions', () => {
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
const o$ = new ObservableClass(observer => {
observer.error('dummy error');
});
const subA = o$.subscribe(obs);
expect(subA.closed).toBe(true);
expect(onError).toBeCalledTimes(1);
expect(onError).toBeCalledWith('dummy error');
const subB = o$.subscribe(obs);
expect(subB.closed).toBe(true);
expect(onError).toBeCalledTimes(2);
expect(onError).toHaveBeenNthCalledWith(2, 'dummy error');
expect(onComplete).not.toBeCalled();
expect(onNext).not.toBeCalled();
});
it('[unsubscribe] should pass the closed boolen to true', () => {
const o$ = new ObservableClass(() => {});
const sub = o$.subscribe({});
expect(sub.closed).toBe(false);
sub.unsubscribe();
expect(sub.closed).toBe(true);
});
it('[unsubscribe] teardown should be called on unsubscribe', () => {
const teardown = jest.fn();
const o$ = new ObservableClass(() => teardown);
expect(teardown).not.toBeCalled();
const sub = o$.subscribe({});
expect(teardown).not.toBeCalled();
sub.unsubscribe();
sub.unsubscribe();
expect(teardown).toBeCalledTimes(1);
o$.subscribe({}).unsubscribe();
expect(teardown).toBeCalledTimes(2);
});
it('[complete] teardown should be called on complete', () => {
const teardown = jest.fn();
const o$ = new ObservableClass(observer => {
observer.complete();
return teardown;
});
const sub = o$.subscribe({});
expect(teardown).toBeCalledTimes(1);
sub.unsubscribe();
expect(teardown).toBeCalledTimes(1);
o$.subscribe({}).unsubscribe();
expect(teardown).toBeCalledTimes(2);
});
it('[error] teardown should be called on error', () => {
const teardown = jest.fn();
const o$ = new ObservableClass(observer => {
observer.error('dummy error');
return teardown;
});
const sub = o$.subscribe({ error: jest.fn() });
expect(teardown).toBeCalledTimes(1);
sub.unsubscribe();
expect(teardown).toBeCalledTimes(1);
o$.subscribe({ error: jest.fn() }).unsubscribe();
expect(teardown).toBeCalledTimes(2);
});
};
describe('RX', () => {
describe('Observable', testObservable(Rx.Observable as unknown as ObservableConstructor));
});
describe('Custom implementation', () => {
describe('Observable', testObservable(Observable));
});

View File

@ -0,0 +1,224 @@
import 'jest-extended';
import * as Rx from 'rxjs';
import { Subject, SubjectConstructor } from '../Subject';
const testSubject = (SubjectClass: SubjectConstructor) => () => {
it('[next] several values', () => {
const subject = new SubjectClass<number>();
const onNext = jest.fn();
const sub = subject.subscribe({ next: onNext });
subject.next(10);
subject.next(20);
subject.next(30);
expect(onNext.mock.calls[0]).toEqual([10]);
expect(onNext.mock.calls[1]).toEqual([20]);
expect(onNext.mock.calls[2]).toEqual([30]);
sub.unsubscribe();
});
it('[next] several values for several subscriber', () => {
const subject = new SubjectClass<number>();
const onNext1 = jest.fn();
const onNext2 = jest.fn();
const sub1 = subject.subscribe({ next: onNext1 });
const sub2 = subject.subscribe({ next: onNext2 });
subject.next(10);
subject.next(20);
subject.next(30);
expect(onNext1.mock.calls[0]).toEqual([10]);
expect(onNext1.mock.calls[1]).toEqual([20]);
expect(onNext1.mock.calls[2]).toEqual([30]);
expect(onNext2.mock.calls[0]).toEqual([10]);
expect(onNext2.mock.calls[1]).toEqual([20]);
expect(onNext2.mock.calls[2]).toEqual([30]);
sub1.unsubscribe();
sub2.unsubscribe();
});
it('[complete] should block next and error', () => {
const subject = new SubjectClass<number>();
const onNext = jest.fn();
const onError = jest.fn();
const sub = subject.subscribe({ next: onNext, error: onError });
subject.complete();
subject.next(1);
subject.error('test');
expect(onNext).not.toBeCalled();
expect(onError).not.toBeCalled();
sub.unsubscribe();
});
it('[complete] should complete', () => {
const subject = new SubjectClass<number>();
const onComplete = jest.fn();
const sub = subject.subscribe({ complete: onComplete });
subject.complete();
expect(onComplete).toBeCalled();
sub.unsubscribe();
});
it('[complete] should complete once', () => {
const subject = new SubjectClass<number>();
const onComplete = jest.fn();
const sub = subject.subscribe({ complete: onComplete });
subject.complete();
subject.complete();
subject.complete();
expect(onComplete).toBeCalledTimes(1);
sub.unsubscribe();
});
it('[complete] should unsubscribe all listeners', () => {
const subject = new SubjectClass<number>();
const teardown1 = jest.fn();
const teardown2 = jest.fn();
const teardown3 = jest.fn();
const sub1 = subject.subscribe({});
const sub2 = subject.subscribe({});
const sub3 = subject.subscribe({});
sub1.add(teardown1);
sub2.add(teardown2);
sub3.add(teardown3);
subject.complete();
expect(teardown1).toHaveBeenCalledTimes(1);
expect(teardown2).toHaveBeenCalledTimes(1);
expect(teardown3).toHaveBeenCalledTimes(1);
expect(sub1.closed).toBe(true);
expect(sub2.closed).toBe(true);
expect(sub3.closed).toBe(true);
});
it('[complete] subscribe on a completed subject should complete immediatly', () => {
const subject = new SubjectClass<number>();
const onComplete = jest.fn();
subject.complete();
const sub = subject.subscribe({ complete: onComplete });
expect(onComplete).toBeCalled();
expect(sub.closed).toBe(true);
});
it('[error] should block next and complete', () => {
const subject = new SubjectClass<number>();
const onNext = jest.fn();
const onComplete = jest.fn();
const onError = jest.fn();
const sub = subject.subscribe({ next: onNext, complete: onComplete, error: onError });
subject.error('test');
subject.next(1);
subject.complete();
expect(onNext).not.toBeCalled();
expect(onComplete).not.toBeCalled();
sub.unsubscribe();
});
it('[error] should emit error once', () => {
const subject = new SubjectClass<number>();
const onError = jest.fn();
const sub = subject.subscribe({ error: onError });
subject.error('test');
subject.error('test');
subject.error('test');
expect(onError).toBeCalledTimes(1);
expect(onError).toBeCalledWith('test');
sub.unsubscribe();
});
it('[error] should unsubscribe all listeners', () => {
const subject = new SubjectClass<number>();
const teardown1 = jest.fn();
const teardown2 = jest.fn();
const teardown3 = jest.fn();
const onError = jest.fn();
const sub1 = subject.subscribe({ error: onError });
const sub2 = subject.subscribe({ error: onError });
const sub3 = subject.subscribe({ error: onError });
sub1.add(teardown1);
sub2.add(teardown2);
sub3.add(teardown3);
subject.error('test');
expect(teardown1).toHaveBeenCalledTimes(1);
expect(teardown2).toHaveBeenCalledTimes(1);
expect(teardown3).toHaveBeenCalledTimes(1);
expect(sub1.closed).toBe(true);
expect(sub2.closed).toBe(true);
expect(sub3.closed).toBe(true);
});
it('[error] subscribe on a completed subject caused by an error should replay this error', () => {
const subject = new SubjectClass<number>();
const onError = jest.fn();
const onComplete = jest.fn();
subject.error('test');
const sub = subject.subscribe({ error: onError, complete: onComplete });
expect(onError).toBeCalledWith('test');
expect(onComplete).not.toBeCalled();
expect(sub.closed).toBe(true);
});
it('[subscription] added teardown should be executed once on complete', () => {
const subject = new SubjectClass<number>();
const teardown = jest.fn();
const sub = subject.subscribe({});
sub.add(teardown);
subject.complete();
expect(teardown).toBeCalled();
sub.unsubscribe();
expect(teardown).toHaveBeenCalledTimes(1);
});
};
describe('RX', () => {
describe('Subject', testSubject(Rx.Subject as unknown as SubjectConstructor));
});
describe('Custom implementation', () => {
describe('Subject', testSubject(Subject));
});

View File

@ -0,0 +1,279 @@
import 'jest-extended';
import * as Rx from 'rxjs';
import { Subscriber, SubscriberConstructor } from '../Subscriber';
import { Observer } from '../Observer';
const getObserver = <T>(): Observer<T> => {
return { next: jest.fn(), error: jest.fn(), complete: jest.fn() };
};
const testSubscriber = (SubscriberClass: SubscriberConstructor) => () => {
it('[closed] should be false before then true on unsubscribe', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
expect(subscriber.closed).toBe(false);
subscriber.unsubscribe();
expect(subscriber.closed).toBe(true);
});
it('[closed] should be false before then true on error', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
expect(subscriber.closed).toBe(false);
subscriber.error('dummy error');
expect(subscriber.closed).toBe(true);
});
it('[closed] should be false before then true on complete', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
expect(subscriber.closed).toBe(false);
subscriber.complete();
expect(subscriber.closed).toBe(true);
});
it('[next] should call onNext with given values', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.next('a');
subscriber.next('b');
subscriber.next('c');
expect(obs.next).toHaveBeenNthCalledWith(1, 'a');
expect(obs.next).toHaveBeenNthCalledWith(2, 'b');
expect(obs.next).toHaveBeenNthCalledWith(3, 'c');
});
it('[next] cannot call next after error', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.next('a');
subscriber.error('dummy error');
subscriber.next('b');
subscriber.next('c');
expect(obs.next).toHaveBeenCalledTimes(1);
});
it('[next] cannot call next after complete', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.next('a');
subscriber.complete();
subscriber.next('b');
subscriber.next('c');
expect(obs.next).toHaveBeenCalledTimes(1);
});
it('[next] cannot call next after unsubscribe', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.next('a');
subscriber.unsubscribe();
subscriber.next('b');
subscriber.next('c');
expect(obs.next).toHaveBeenCalledTimes(1);
});
it('[error] should call onError (once) with given error', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.error('dummy error');
subscriber.error('dummy error 2 (should be ignored');
expect(obs.error).toHaveBeenCalledTimes(1);
expect(obs.error).toHaveBeenCalledWith('dummy error');
});
it('[error] should call onError before unsubscribe', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
const onUnsubscribe = jest.fn();
subscriber.add(onUnsubscribe);
subscriber.error('dummy error');
subscriber.error('dummy error 2 (should be ignored');
expect(obs.error).toHaveBeenCalledTimes(1);
expect(obs.error).toHaveBeenCalledWith('dummy error');
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
expect(obs.error).toHaveBeenCalledBefore(onUnsubscribe);
expect(obs.error).toHaveBeenCalledBefore(onUnsubscribe);
});
it('[error] should ignore errors when completed', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.complete();
subscriber.error('dummy error (should be ignored)');
subscriber.error('dummy error 2 (should be ignored');
expect(obs.error).toHaveBeenCalledTimes(0);
});
it('[error] should ignore errors after unsubscribe', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.unsubscribe();
subscriber.error('dummy error (should be ignored)');
subscriber.error('dummy error 2 (should be ignored');
expect(obs.error).toHaveBeenCalledTimes(0);
});
it('[complete] should call onComplete (once)', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.complete();
subscriber.complete();
expect(obs.complete).toHaveBeenCalledTimes(1);
});
it('[complete] should call onComplete before unsubscribe', () => {
const onUnsubscribe = jest.fn();
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.add(onUnsubscribe);
subscriber.complete();
expect(obs.complete).toHaveBeenCalledTimes(1);
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
expect(obs.complete).toHaveBeenCalledBefore(onUnsubscribe);
});
it('[complete] should not call onComplete after error', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.error('dummy error');
subscriber.complete();
expect(obs.complete).toHaveBeenCalledTimes(0);
});
it('[complete] should not call onComplete after unsubscribe', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.unsubscribe();
subscriber.complete();
expect(obs.complete).toHaveBeenCalledTimes(0);
});
it('[unsubscribe] should not call onNext/onError/onComplete', () => {
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.unsubscribe();
expect(obs.next).not.toBeCalled();
expect(obs.error).not.toBeCalled();
expect(obs.complete).not.toBeCalled();
});
it('[add] added teardowns are called once on unsubscribe', () => {
const teardownA = jest.fn();
const teardownB = jest.fn();
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.add(teardownA);
subscriber.add(teardownB);
subscriber.unsubscribe();
subscriber.unsubscribe();
expect(teardownA).toBeCalledTimes(1);
expect(teardownB).toBeCalledTimes(1);
});
it('[add] added teardowns are called once on complete', () => {
const teardownA = jest.fn();
const teardownB = jest.fn();
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.add(teardownA);
subscriber.add(teardownB);
subscriber.complete();
subscriber.complete();
expect(teardownA).toBeCalledTimes(1);
expect(teardownB).toBeCalledTimes(1);
});
it('[add] added teardowns are called once on error', () => {
const teardownA = jest.fn();
const teardownB = jest.fn();
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.add(teardownA);
subscriber.add(teardownB);
subscriber.error('dummy error');
subscriber.error('dummy error 2');
expect(teardownA).toBeCalledTimes(1);
expect(teardownB).toBeCalledTimes(1);
});
it('[remove] removed teardowns are not called on unsubscribe', () => {
const subA = { unsubscribe: jest.fn() };
const subB = { unsubscribe: jest.fn() };
const obs = getObserver();
const subscriber = new SubscriberClass(obs);
subscriber.add(subA);
subscriber.add(subB);
subscriber.remove(subA);
subscriber.remove(subB);
subscriber.unsubscribe();
expect(subA.unsubscribe).not.toBeCalled();
expect(subB.unsubscribe).not.toBeCalled();
});
};
describe('RX', () => {
describe('Subscriber', testSubscriber(Rx.Subscriber as SubscriberConstructor));
});
describe('Custom implementation', () => {
describe('Subscriber', testSubscriber(Subscriber));
});

View File

@ -0,0 +1,167 @@
import 'jest-extended';
import * as Rx from 'rxjs';
import { SubscriptionConstructor, Subscription } from '../Subscription';
const testSubscription = (SubscriptionClass: SubscriptionConstructor) => () => {
it('[constructor] should not throw when created', () => {
expect(() => new SubscriptionClass()).not.toThrow();
});
it('[closed] should be false on creation', () => {
expect(new SubscriptionClass().closed).toBe(false);
});
it('[closed] should be true after unsubscribe', () => {
const sub = new SubscriptionClass();
sub.unsubscribe();
expect(sub.closed).toBe(true);
});
it('[unsubscribe] should call the given teardown', () => {
const teardown = jest.fn();
const sub = new SubscriptionClass(teardown);
sub.unsubscribe();
expect(teardown).toBeCalledTimes(1);
expect(sub.closed).toBe(true);
});
it('[unsubscribe] should call the given teardonwn once', () => {
const teardown = jest.fn();
const sub = new SubscriptionClass(teardown);
sub.unsubscribe();
sub.unsubscribe();
expect(teardown).toBeCalledTimes(1);
});
it('[add] should execute added teardowns when subscription is closed', () => {
const initialTeardown = jest.fn();
const additionalTeardown = jest.fn();
const sub = new SubscriptionClass(initialTeardown);
sub.add(additionalTeardown);
sub.unsubscribe();
expect(initialTeardown).toBeCalledTimes(1);
expect(additionalTeardown).toBeCalledTimes(1);
});
it('[add] should return a new subscription when adding a teardown', () => {
const parentSub = new SubscriptionClass();
const childSub = parentSub.add(Rx.noop);
expect(parentSub).not.toBe(childSub);
});
it('[add] should be able to unsubscribe partially using child subscription', () => {
const parentTeardown = jest.fn();
const childTeardown = jest.fn();
const childSub = new SubscriptionClass(childTeardown);
const parentSub = new SubscriptionClass(parentTeardown);
parentSub.add(childSub);
childSub.unsubscribe();
// childSub.unsubscribe();
expect(childTeardown).toBeCalledTimes(1);
expect(parentTeardown).not.toBeCalled();
parentSub.unsubscribe();
expect(childTeardown).toBeCalledTimes(1);
expect(parentTeardown).toBeCalledTimes(1);
});
it('[add] should add several teardowns', () => {
const teardownA = jest.fn();
const teardownB = jest.fn();
const teardownC = jest.fn();
const sub = new SubscriptionClass();
sub.add(teardownA);
sub.add(teardownB);
sub.add(teardownC);
sub.unsubscribe();
expect(teardownA).toBeCalledTimes(1);
expect(teardownB).toBeCalledTimes(1);
expect(teardownC).toBeCalledTimes(1);
});
it('[add] should call a child teardown twice', () => {
const teardown = jest.fn();
const subA = new SubscriptionClass();
const subB = new SubscriptionClass();
subA.add(teardown);
subB.add(teardown);
subA.unsubscribe();
subB.unsubscribe();
expect(teardown).toBeCalledTimes(2);
});
it('[add] should call a child subscription once', () => {
const teardown = jest.fn();
const teardownSub = new SubscriptionClass(teardown);
const subA = new SubscriptionClass();
const subB = new SubscriptionClass();
subA.add(teardownSub);
subB.add(teardownSub);
subA.unsubscribe();
subB.unsubscribe();
expect(teardown).toBeCalledTimes(1);
});
it('[add] should call a new added teardown when already unsubscribed', () => {
const initialTeardown = jest.fn();
const sub = new SubscriptionClass(initialTeardown);
sub.unsubscribe();
expect(initialTeardown).toBeCalledTimes(1);
const additionalTeardown = jest.fn();
sub.add(additionalTeardown);
expect(additionalTeardown).toBeCalledTimes(1);
});
it('[remove] should be able to remove a child subscription', () => {
const parentTeardown = jest.fn();
const childTeardown = jest.fn();
const childSub = new SubscriptionClass(childTeardown);
const parentSub = new SubscriptionClass(parentTeardown);
parentSub.add(childSub);
parentSub.remove(childSub);
parentSub.unsubscribe();
expect(childTeardown).not.toBeCalled();
expect(parentTeardown).toBeCalledTimes(1);
childSub.unsubscribe();
expect(childTeardown).toBeCalledTimes(1);
expect(parentTeardown).toBeCalledTimes(1);
});
};
describe('RX', () => {
describe('Subscription', testSubscription(Rx.Subscription as unknown as SubscriptionConstructor));
});
describe('Custom implementation', () => {
describe('Subscription', testSubscription(Subscription));
});

View File

@ -0,0 +1,63 @@
import * as Rx from 'rxjs';
import * as RxOps from 'rxjs/operators';
import { finalize } from '../../operators/finalize';
import { dummyErrorObservable$, dummyObservable$ } from '../../helpers/test-helpers';
import { Observable } from '../../Observable';
type finalizeOperator = typeof finalize;
const NEVER = new Rx.Observable(() => {}) as unknown as Observable<never>;
const testFinalize = (finalizeOp: finalizeOperator) => () => {
it('finalize on completed', () => {
const finalized = jest.fn();
const result$ = finalizeOp(finalized)(dummyObservable$);
const onNext = jest.fn();
const onComplete = jest.fn();
const sub = result$.subscribe({ next: onNext, complete: onComplete });
expect(finalized).toHaveBeenCalledTimes(1);
sub.unsubscribe();
});
it('finalize on error', async () => {
const finalized = jest.fn();
const result$ = finalizeOp(finalized)(dummyErrorObservable$);
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const sub = result$.subscribe({ next: onNext, error: onError, complete: onComplete });
expect(finalized).toHaveBeenCalledTimes(1);
sub.unsubscribe();
});
it('finalize when unsubscribed', () => {
const finalized = jest.fn();
const result$ = finalizeOp(finalized)(NEVER);
const sub = result$.subscribe({});
expect(finalized).toHaveBeenCalledTimes(0);
sub.unsubscribe();
expect(finalized).toHaveBeenCalledTimes(1);
});
};
describe('RX', () => {
describe('finalize', testFinalize(RxOps.finalize as unknown as finalizeOperator));
});
describe('Custom implementation', () => {
describe('finalize', testFinalize(finalize));
});

View File

@ -0,0 +1,75 @@
import * as RxOps from 'rxjs/operators';
import { map } from '../../operators/map';
import { dummyErrorObservable$, dummyObservable$ } from '../../helpers/test-helpers';
type MapOperator = typeof map;
const testMap = (mapOp: MapOperator) => () => {
it('should map values then complete', () => {
const result$ = mapOp<number, number>(x => x * 2)(dummyObservable$);
const onNext = jest.fn();
const onComplete = jest.fn();
const sub = result$.subscribe({ next: onNext, complete: onComplete });
expect(onNext.mock.calls[0]).toEqual([2]);
expect(onNext.mock.calls[1]).toEqual([4]);
expect(onNext.mock.calls[2]).toEqual([8]);
expect(onNext).toHaveBeenCalledTimes(3);
expect(onComplete).toHaveBeenCalledTimes(1);
sub.unsubscribe();
});
it('should forward error from the source', () => {
const result$ = mapOp<number, number>(x => x * 2)(dummyErrorObservable$);
const onError = jest.fn();
const sub = result$.subscribe({ error: onError });
expect(onError).toHaveBeenCalledWith('test');
expect(onError).toHaveBeenCalledTimes(1);
sub.unsubscribe();
});
it('should forward error from the map project function', () => {
const result$ = mapOp<number, number>(() => {
throw 'woops';
})(dummyObservable$);
const onError = jest.fn();
const sub = result$.subscribe({ error: onError });
expect(onError).toHaveBeenCalledWith('woops');
expect(onError).toHaveBeenCalledTimes(1);
sub.unsubscribe();
});
it('should have a cold behavior', () => {
const onNext = jest.fn();
const mockedProjectFn = jest.fn((x: number) => x * 2);
const result$ = mapOp<number, number>(mockedProjectFn)(dummyObservable$);
const sub1 = result$.subscribe({ next: onNext });
const sub2 = result$.subscribe({ next: onNext });
expect(mockedProjectFn).toBeCalledTimes(6);
expect(onNext.mock.calls).toEqual([[2], [4], [8], [2], [4], [8]]);
sub1.unsubscribe();
sub2.unsubscribe();
});
};
describe('RX', () => {
describe('map', testMap(RxOps.map as unknown as MapOperator));
});
describe('Custom implementation', () => {
describe('map', testMap(map));
});

View File

@ -0,0 +1,98 @@
import * as Rx from 'rxjs';
import * as RxOps from 'rxjs/operators';
import { Observable } from '../../Observable';
import { retry } from '../../operators/retry';
import { dummyObservable$ } from '../../helpers/test-helpers';
type RetryOperator = typeof retry;
const testRetry = (retryOp: RetryOperator) => () => {
it('should next values then complete', () => {
const final$ = retryOp()(dummyObservable$);
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const sub = final$.subscribe({
next: onNext,
error: onError,
complete: onComplete,
});
expect(onNext.mock.calls[0]).toEqual([1]);
expect(onNext.mock.calls[1]).toEqual([2]);
expect(onNext.mock.calls[2]).toEqual([4]);
expect(onNext).toHaveBeenCalledTimes(3);
expect(onComplete).toHaveBeenCalledTimes(1);
sub.unsubscribe();
});
it('should retry the observable on error indefinitely until complete', () => {
let i = 0;
const main$ = new Rx.Observable<number>(obs => {
i = i + 1;
if (i <= 3) {
obs.next(i);
obs.error('test');
}
}) as unknown as Observable<number>;
const final$ = retryOp()(main$);
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const sub = final$.subscribe({
next: onNext,
error: onError,
complete: onComplete,
});
expect(onNext.mock.calls).toEqual([[1], [2], [3]]);
expect(onError).not.toBeCalled();
expect(onComplete).not.toBeCalled();
sub.unsubscribe();
});
it('should retry the observable on error three times then emit the error', () => {
const main$ = new Rx.Observable<number>(obs => {
obs.next(42);
obs.error('test');
}) as unknown as Observable<number>;
const final$ = retryOp(3)(main$);
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const sub = final$.subscribe({
next: onNext,
error: onError,
complete: onComplete,
});
// initial value + 3 replayed values
expect(onNext.mock.calls).toEqual([[42], [42], [42], [42]]);
expect(onError).toBeCalledTimes(1);
expect(onError).toBeCalledWith('test');
expect(onComplete).not.toBeCalled();
sub.unsubscribe();
});
};
describe('RX', () => {
describe('map', testRetry(RxOps.retry as unknown as RetryOperator));
});
describe('Custom implementation', () => {
describe('map', testRetry(retry));
});

View File

@ -0,0 +1,88 @@
import * as RxOps from 'rxjs/operators';
import { scan } from '../../operators/scan';
import { dummyErrorObservable$, dummyObservable$ } from '../../helpers/test-helpers';
type ScanOperator = typeof scan;
/**
* Tests
*/
const testScan = (scanOp: ScanOperator) => () => {
it('should scan values with a sum function then complete', () => {
const result$ = scanOp<number, number>((acc, value) => acc + value, 0)(dummyObservable$);
const onNext = jest.fn();
const onComplete = jest.fn();
const sub = result$.subscribe({
next: onNext,
complete: onComplete,
});
expect(onNext.mock.calls[0]).toEqual([1]);
expect(onNext.mock.calls[1]).toEqual([3]);
expect(onNext.mock.calls[2]).toEqual([7]);
expect(onNext).toHaveBeenCalledTimes(3);
expect(onComplete).toHaveBeenCalledTimes(1);
sub.unsubscribe();
});
it('should forward error from the source', () => {
const result$ = scanOp<number, number>((acc, value) => acc + value, 0)(dummyErrorObservable$);
const onError = jest.fn();
const sub = result$.subscribe({
error: onError,
});
expect(onError).toHaveBeenCalledWith('test');
expect(onError).toHaveBeenCalledTimes(1);
sub.unsubscribe();
});
it('should forward error from the map project function', () => {
const result$ = scanOp<number, number>(() => {
throw 'woops';
}, 0)(dummyObservable$);
const onError = jest.fn();
const sub = result$.subscribe({
error: onError,
});
expect(onError).toHaveBeenCalledWith('woops');
expect(onError).toHaveBeenCalledTimes(1);
sub.unsubscribe();
});
it('should have a cold behavior', () => {
const onNext = jest.fn();
const mockedFn = jest.fn();
const result$ = scanOp<number, number>((acc, value) => {
mockedFn();
return acc + value;
}, 0)(dummyObservable$);
const sub1 = result$.subscribe({ next: onNext });
const sub2 = result$.subscribe({ next: onNext });
expect(mockedFn).toBeCalledTimes(6);
expect(onNext.mock.calls).toEqual([[1], [3], [7], [1], [3], [7]]);
sub1.unsubscribe();
sub2.unsubscribe();
});
};
describe('RX', () => {
describe('scan', testScan(RxOps.scan as unknown as ScanOperator));
});
describe('Custom implementation', () => {
describe('scan', testScan(scan));
});

View File

@ -0,0 +1,237 @@
import * as Rx from 'rxjs';
import * as RxOps from 'rxjs/operators';
import { Observable } from '../../Observable';
import { switchMap } from '../../operators/switchMap';
import { createSubject, dummyObservable$, of, toObservable } from '../../helpers/test-helpers';
type SwitchMapOperator = typeof switchMap;
/**
* Tests
*/
const testSwitchMap = (switchMapOp: SwitchMapOperator) => () => {
it('emit values through the inner observable then complete', () => {
const final$ = switchMapOp<Observable<number>, number>(x => x)(of(dummyObservable$));
const onNext = jest.fn();
const onError = jest.fn();
const onComplete = jest.fn();
const sub = final$.subscribe({
next: onNext,
error: onError,
complete: onComplete,
});
expect(onNext.mock.calls[0]).toEqual([1]);
expect(onNext.mock.calls[1]).toEqual([2]);
expect(onNext.mock.calls[2]).toEqual([4]);
expect(onNext).toBeCalledTimes(3);
expect(onError).not.toHaveBeenCalled();
expect(onComplete).toBeCalledTimes(1);
expect(sub.closed).toBe(true);
});
it('should continue emit values through the inner observable even when source is completed', () => {
const inner$ = createSubject<number>();
const final$ = switchMapOp<Observable<number>, number>(x => x)(of(toObservable(inner$)));
const onNext = jest.fn();
const sub = final$.subscribe({
next: onNext,
});
expect(onNext).not.toBeCalled();
inner$.next(1);
inner$.next(2);
inner$.next(3);
expect(onNext).toBeCalledTimes(3);
expect(onNext.mock.calls).toEqual([[1], [2], [3]]);
sub.unsubscribe();
});
it('emit values through multiple inner observable', () => {
const main$ = createSubject<number>();
const final$ = switchMapOp<number, number>(x => {
return new Rx.Observable(obs => {
obs.next(x);
obs.complete();
}) as unknown as Observable<number>;
})(toObservable(main$));
const onNext = jest.fn();
const onComplete = jest.fn();
const sub = final$.subscribe({
next: onNext,
complete: onComplete,
});
expect(onNext).not.toBeCalled();
expect(onComplete).not.toBeCalled();
main$.next(1);
main$.next(2);
main$.next(3);
expect(onNext.mock.calls).toEqual([[1], [2], [3]]);
expect(onComplete).not.toBeCalled();
main$.complete();
expect(onComplete).toBeCalled();
expect(sub.closed).toBe(true);
});
it('unsubscribe previous inner observable when a new value is emitted', () => {
const main$ = createSubject<number>();
const onSubscribe = jest.fn();
const onUnsubscribe = jest.fn();
const final$ = switchMapOp<number, number>(() => {
return new Rx.Observable(() => {
onSubscribe();
return onUnsubscribe;
}) as unknown as Observable<number>;
})(toObservable(main$));
const sub = final$.subscribe({});
expect(onSubscribe).not.toBeCalled();
expect(onUnsubscribe).not.toBeCalled();
main$.next(42);
expect(onSubscribe).toHaveBeenCalledTimes(1);
expect(onUnsubscribe).not.toBeCalled();
main$.next(42);
expect(onSubscribe).toHaveBeenCalledTimes(2);
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
sub.unsubscribe();
});
it('unsubscribe inner observable when source is unsubscribed', () => {
const main$ = createSubject<number>();
const onSubscribe = jest.fn();
const onUnsubscribe = jest.fn();
const final$ = switchMapOp<number, number>(() => {
return new Rx.Observable(() => {
onSubscribe();
return onUnsubscribe;
}) as unknown as Observable<number>;
})(toObservable(main$));
const sub = final$.subscribe({});
main$.next(42);
sub.unsubscribe();
expect(onSubscribe).toHaveBeenCalledTimes(1);
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
});
it('should unsubscribe only when source complete AND inner observable complete', () => {
const inner$ = createSubject<number>();
const main$ = createSubject<number>();
const final$ = switchMapOp<number, number>(() => {
return toObservable(inner$);
})(toObservable(main$));
const sub = final$.subscribe({});
main$.next(42);
main$.complete();
expect(sub.closed).toBe(false);
inner$.complete();
expect(sub.closed).toBe(true);
});
it('should unsubscribe only when source complete AND inner observable emit an error', () => {
const inner$ = createSubject<number>();
const main$ = createSubject<number>();
const onError = jest.fn();
const final$ = switchMapOp<number, number>(() => {
return toObservable(inner$);
})(toObservable(main$));
const sub = final$.subscribe({ error: onError });
main$.next(42);
main$.complete();
expect(sub.closed).toBe(false);
inner$.error('test');
expect(sub.closed).toBe(true);
});
it('unsubscribe inner observable when source emit an error', () => {
const main$ = createSubject<number>();
const onError = jest.fn();
const onSubscribe = jest.fn();
const onUnsubscribe = jest.fn();
const final$ = switchMapOp<number, number>(() => {
return new Rx.Observable(() => {
onSubscribe();
return onUnsubscribe;
}) as unknown as Observable<number>;
})(toObservable(main$));
const sub = final$.subscribe({ error: onError });
main$.next(42);
main$.error('test');
expect(onSubscribe).toHaveBeenCalledTimes(1);
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
expect(sub.closed).toBe(true);
});
it('emit an error when throw an error from the project function', () => {
const main$ = createSubject<number>();
const onError = jest.fn();
const final$ = switchMapOp<number, number>(() => {
throw 'test';
})(toObservable(main$));
const sub = final$.subscribe({
error: onError,
});
main$.next(42);
expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith('test');
expect(sub.closed).toBe(true);
});
};
describe('RX', () => {
describe('switchMap', testSwitchMap(RxOps.switchMap as unknown as SwitchMapOperator));
});
describe('Custom implementation', () => {
describe('switchMap', testSwitchMap(switchMap));
});

View File

@ -0,0 +1,30 @@
import * as Rx from 'rxjs';
import { Observable } from '../Observable';
import { Subject } from '../Subject';
/**
* Mocks
*/
export const dummyObservable$ = new Rx.Observable<number>(obs => {
obs.next(1);
obs.next(2);
obs.next(4);
obs.complete();
}) as unknown as Observable<number>;
export const dummyErrorObservable$ = new Rx.Observable<number>(obs => {
obs.error('test');
}) as unknown as Observable<number>;
/**
* Additional helpers (for unit tests)
*/
export const of = <T>(value: T): Observable<T> => Rx.of(value) as unknown as Observable<T>;
export const createSubject = <T>(): Subject<T> => {
return new Rx.Subject<T>() as unknown as Subject<T>;
};
export const toObservable = <T>(s: Subject<T>): Observable<T> => {
return s as unknown as Observable<T>;
};

View File

@ -0,0 +1,8 @@
import { Observable } from '../Observable';
import { OperatorFunction } from './types';
export function finalize<T>(callback: () => void): OperatorFunction<T, T> {
void Observable;
void callback;
throw new Error('TODO');
}

8
src/operators/map.ts Normal file
View File

@ -0,0 +1,8 @@
import { Observable } from '../Observable';
import { OperatorFunction } from './types';
export function map<T, R>(project: (value: T) => R): OperatorFunction<T, R> {
void Observable;
void project;
throw new Error('TODO');
}

8
src/operators/retry.ts Normal file
View File

@ -0,0 +1,8 @@
import { Observable } from '../Observable';
import { OperatorFunction } from './types';
export function retry<T>(limit?: number): OperatorFunction<T, T> {
void Observable;
void limit;
throw new Error('TODO');
}

8
src/operators/scan.ts Normal file
View File

@ -0,0 +1,8 @@
import { Observable } from '../Observable';
import { OperatorFunction } from './types';
export function scan<T, R>(accumulator: (acc: R, value: T) => R, seed: R): OperatorFunction<T, R> {
void Observable;
void accumulator, seed;
throw new Error('TODO');
}

View File

@ -0,0 +1,7 @@
import { Observable } from '../Observable';
import { OperatorFunction } from './types';
export function switchMap<T, R>(project: (value: T) => Observable<R>): OperatorFunction<T, R> {
void project;
throw new Error('TODO');
}

3
src/operators/types.ts Normal file
View File

@ -0,0 +1,3 @@
import { Observable } from '../Observable';
export type OperatorFunction<T, R> = (observable: Observable<T>) => Observable<R>;

8
src/tsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "@trapcodien/tsconfig/tsconfig.base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "../dist"
},
"references": [{ "path": "../" }]
}

10
tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"rootDir": ".",
"outDir": ".",
"resolveJsonModule": true,
"composite": true,
"esModuleInterop": true
},
"files": ["package.json"]
}

4225
yarn.lock Normal file

File diff suppressed because it is too large Load Diff