chore: init repository
This commit is contained in:
commit
697b09b8d4
77
.drone.yml
Normal file
77
.drone.yml
Normal 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
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
/dist/
|
||||||
107
.gitignore
vendored
Normal file
107
.gitignore
vendored
Normal 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
1
.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
@garm:registry=https://git.trapcodien.com/api/packages/garm/npm/
|
||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
/dist/
|
||||||
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
"@trapcodien/prettier-config"
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||||
14
cucumber.js
Normal file
14
cucumber.js
Normal 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
7
jest.config.js
Normal 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
59
package.json
Normal 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
33
src/Observable.ts
Normal 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
9
src/Observer.ts
Normal 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
39
src/Subject.ts
Normal 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
39
src/Subscriber.ts
Normal 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
45
src/Subscription.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
311
src/__tests__/Observable.spec.ts
Normal file
311
src/__tests__/Observable.spec.ts
Normal 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));
|
||||||
|
});
|
||||||
224
src/__tests__/Subject.spec.ts
Normal file
224
src/__tests__/Subject.spec.ts
Normal 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));
|
||||||
|
});
|
||||||
279
src/__tests__/Subscriber.spec.ts
Normal file
279
src/__tests__/Subscriber.spec.ts
Normal 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));
|
||||||
|
});
|
||||||
167
src/__tests__/Subscription.spec.ts
Normal file
167
src/__tests__/Subscription.spec.ts
Normal 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));
|
||||||
|
});
|
||||||
63
src/__tests__/operators/finalize.spec.ts
Normal file
63
src/__tests__/operators/finalize.spec.ts
Normal 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));
|
||||||
|
});
|
||||||
75
src/__tests__/operators/map.spec.ts
Normal file
75
src/__tests__/operators/map.spec.ts
Normal 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));
|
||||||
|
});
|
||||||
98
src/__tests__/operators/retry.spec.ts
Normal file
98
src/__tests__/operators/retry.spec.ts
Normal 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));
|
||||||
|
});
|
||||||
88
src/__tests__/operators/scan.spec.ts
Normal file
88
src/__tests__/operators/scan.spec.ts
Normal 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));
|
||||||
|
});
|
||||||
237
src/__tests__/operators/switchMap.spec.ts
Normal file
237
src/__tests__/operators/switchMap.spec.ts
Normal 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));
|
||||||
|
});
|
||||||
30
src/helpers/test-helpers.ts
Normal file
30
src/helpers/test-helpers.ts
Normal 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>;
|
||||||
|
};
|
||||||
8
src/operators/finalize.ts
Normal file
8
src/operators/finalize.ts
Normal 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
8
src/operators/map.ts
Normal 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
8
src/operators/retry.ts
Normal 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
8
src/operators/scan.ts
Normal 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');
|
||||||
|
}
|
||||||
7
src/operators/switchMap.ts
Normal file
7
src/operators/switchMap.ts
Normal 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
3
src/operators/types.ts
Normal 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
8
src/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "@trapcodien/tsconfig/tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "../dist"
|
||||||
|
},
|
||||||
|
"references": [{ "path": "../" }]
|
||||||
|
}
|
||||||
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": ".",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"composite": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"files": ["package.json"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user