coomer-downloader 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +14 -3
  2. package/biome.json +6 -4
  3. package/dist/index.js +539 -285
  4. package/docs/images/Screenshot 01.jpg +0 -0
  5. package/package.json +14 -5
  6. package/src/api/bunkr.ts +1 -1
  7. package/src/api/coomer-api.ts +23 -6
  8. package/src/api/gofile.ts +2 -2
  9. package/src/api/index.ts +5 -1
  10. package/src/api/nsfw.xxx.ts +3 -3
  11. package/src/api/plain-curl.ts +1 -1
  12. package/src/{args-handler.ts → cli/args-handler.ts} +17 -12
  13. package/src/cli/ui/app.tsx +40 -0
  14. package/src/cli/ui/components/file.tsx +44 -0
  15. package/src/cli/ui/components/filelist.tsx +52 -0
  16. package/src/cli/ui/components/index.ts +6 -0
  17. package/src/cli/ui/components/keyboardinfo.tsx +41 -0
  18. package/src/cli/ui/components/loading.tsx +20 -0
  19. package/src/cli/ui/components/preview.tsx +32 -0
  20. package/src/cli/ui/components/spinner.tsx +28 -0
  21. package/src/cli/ui/components/titlebar.tsx +15 -0
  22. package/src/cli/ui/hooks/downloader.ts +21 -0
  23. package/src/cli/ui/hooks/input.ts +17 -0
  24. package/src/cli/ui/index.tsx +7 -0
  25. package/src/cli/ui/store/index.ts +19 -0
  26. package/src/index.ts +42 -23
  27. package/src/logger/index.ts +15 -0
  28. package/src/services/downloader.ts +161 -0
  29. package/src/services/file.ts +113 -0
  30. package/src/types/index.ts +16 -1
  31. package/src/utils/duplicates.ts +23 -0
  32. package/src/utils/filters.ts +15 -15
  33. package/src/utils/io.ts +25 -0
  34. package/src/utils/mediatypes.ts +13 -0
  35. package/src/utils/promise.ts +0 -50
  36. package/src/utils/requests.ts +2 -2
  37. package/src/utils/strings.ts +1 -10
  38. package/src/utils/timer.ts +11 -9
  39. package/tsconfig.json +2 -1
  40. package/src/utils/downloader.ts +0 -108
  41. package/src/utils/file.ts +0 -75
  42. package/src/utils/index.ts +0 -11
  43. package/src/utils/multibar.ts +0 -62
package/src/index.ts CHANGED
@@ -1,36 +1,55 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --no-warnings=ExperimentalWarning
2
+
2
3
  import process from 'node:process';
3
4
  import { apiHandler } from './api';
4
- import { argumentHander } from './args-handler';
5
- import { createMultibar, Downloader, setGlobalHeaders } from './utils';
5
+ import { argumentHander } from './cli/args-handler';
6
+ import { createReactInk } from './cli/ui';
7
+ import { useInkStore } from './cli/ui/store';
8
+ import { Downloader } from './services/downloader';
9
+ import { parseSizeValue } from './utils/filters';
10
+ import { setGlobalHeaders } from './utils/requests';
6
11
 
7
12
  async function run() {
8
- const { url, dir, media, include, exclude, skip } = argumentHander();
13
+ createReactInk();
14
+
15
+ const { url, dir, media, include, exclude, minSize, maxSize, skip, removeDupilicates } =
16
+ argumentHander();
9
17
 
10
18
  const filelist = await apiHandler(url);
11
19
 
12
- const found = filelist.files.length;
13
- filelist.setDirPath(dir);
14
- filelist.skip(skip);
15
- filelist.filterByText(include, exclude);
16
- filelist.filterByMediaType(media);
17
-
18
- console.table([
19
- {
20
- found,
21
- skip,
22
- filtered: found - filelist.files.length,
23
- folder: filelist.dirPath,
24
- },
25
- ]);
20
+ filelist
21
+ .setDirPath(dir)
22
+ .skip(skip)
23
+ .filterByText(include, exclude)
24
+ .filterByMediaType(media);
25
+
26
+ if (removeDupilicates) {
27
+ filelist.removeURLDuplicates();
28
+ }
29
+
30
+ const minSizeBytes = minSize ? parseSizeValue(minSize) : undefined;
31
+ const maxSizeBytes = maxSize ? parseSizeValue(maxSize) : undefined;
32
+
33
+ await filelist.calculateFileSizes();
26
34
 
27
35
  setGlobalHeaders({ Referer: url });
28
36
 
29
- const downloader = new Downloader();
30
- createMultibar(downloader);
31
- await downloader.downloadFiles(filelist);
37
+ const downloader = new Downloader(filelist, minSizeBytes, maxSizeBytes);
38
+ useInkStore.getState().setDownloader(downloader);
39
+
40
+ await downloader.downloadFiles();
32
41
 
33
- process.kill(process.pid, 'SIGINT');
42
+ if (removeDupilicates) {
43
+ await filelist.removeDuplicatesByHash();
44
+ }
34
45
  }
35
46
 
36
- run();
47
+ (async () => {
48
+ try {
49
+ await run();
50
+ process.exit(0);
51
+ } catch (err) {
52
+ console.error('Fatal error:', err);
53
+ process.exit(1);
54
+ }
55
+ })();
@@ -0,0 +1,15 @@
1
+ import pino from 'pino';
2
+
3
+ // tail -f debug.log | jq -C .
4
+ const logger = pino(
5
+ {
6
+ level: 'debug',
7
+ },
8
+ pino.destination({
9
+ dest: './debug.log',
10
+ append: false,
11
+ sync: true,
12
+ }),
13
+ );
14
+
15
+ export default logger;
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import { Readable, Transform } from 'node:stream';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { Subject } from 'rxjs';
5
+ import { tryFixCoomerUrl } from '../api/coomer-api';
6
+ import type { AbortControllerSubject, DownloaderSubject } from '../types';
7
+ import { deleteFile, getFileSize, mkdir } from '../utils/io';
8
+ import { sleep } from '../utils/promise';
9
+ import { fetchByteRange } from '../utils/requests';
10
+ import { Timer } from '../utils/timer';
11
+ import type { CoomerFile, CoomerFileList } from './file';
12
+
13
+ export class Downloader {
14
+ public subject = new Subject<DownloaderSubject>();
15
+
16
+ private abortController = new AbortController();
17
+ public abortControllerSubject = new Subject<AbortControllerSubject>();
18
+
19
+ setAbortControllerListener() {
20
+ this.abortControllerSubject.subscribe((type) => {
21
+ this.abortController.abort(type);
22
+ this.abortController = new AbortController();
23
+ });
24
+ }
25
+
26
+ constructor(
27
+ public filelist: CoomerFileList,
28
+ public minSize?: number,
29
+ public maxSize?: number,
30
+ public chunkTimeout = 30_000,
31
+ public chunkFetchRetries = 5,
32
+ public fetchRetries = 7,
33
+ ) {
34
+ this.setAbortControllerListener();
35
+ }
36
+
37
+ async fetchStream(
38
+ file: CoomerFile,
39
+ stream: Readable,
40
+ sizeOld = 0,
41
+ retries = this.chunkFetchRetries,
42
+ ): Promise<void> {
43
+ const signal = this.abortController.signal;
44
+ const subject = this.subject;
45
+ const { timer } = Timer.withAbortController(
46
+ this.chunkTimeout,
47
+ this.abortControllerSubject,
48
+ );
49
+
50
+ try {
51
+ const fileStream = fs.createWriteStream(file.filepath as string, { flags: 'a' });
52
+
53
+ const progressStream = new Transform({
54
+ transform(chunk, _encoding, callback) {
55
+ this.push(chunk);
56
+ file.downloaded += chunk.length;
57
+ timer.reset();
58
+ subject.next({ type: 'CHUNK_DOWNLOADING_UPDATE' });
59
+ callback();
60
+ },
61
+ });
62
+
63
+ subject.next({ type: 'CHUNK_DOWNLOADING_START' });
64
+ await pipeline(stream, progressStream, fileStream, { signal });
65
+ } catch (error) {
66
+ if (signal.aborted) {
67
+ if (signal.reason === 'FILE_SKIP') return;
68
+ if (signal.reason === 'TIMEOUT') {
69
+ if (retries === 0 && sizeOld < file.downloaded) {
70
+ retries += this.chunkFetchRetries;
71
+ sizeOld = file.downloaded;
72
+ }
73
+ if (retries === 0) return;
74
+ return await this.fetchStream(file, stream, sizeOld, retries - 1);
75
+ }
76
+ }
77
+ throw error;
78
+ } finally {
79
+ subject.next({ type: 'CHUNK_DOWNLOADING_END' });
80
+ timer.stop();
81
+ }
82
+ }
83
+
84
+ public skip() {
85
+ this.abortControllerSubject.next('FILE_SKIP');
86
+ }
87
+
88
+ private filterFileSize(file: CoomerFile) {
89
+ if (!file.size) return;
90
+ if (
91
+ (this.minSize && file.size < this.minSize) ||
92
+ (this.maxSize && file.size > this.maxSize)
93
+ ) {
94
+ try {
95
+ deleteFile(file.filepath);
96
+ } catch {}
97
+ this.skip();
98
+ return;
99
+ }
100
+ }
101
+
102
+ async downloadFile(file: CoomerFile, retries = this.fetchRetries): Promise<void> {
103
+ const signal = this.abortController.signal;
104
+ try {
105
+ file.downloaded = await getFileSize(file.filepath as string);
106
+
107
+ const response = await fetchByteRange(file.url, file.downloaded, signal);
108
+
109
+ if (!response?.ok && response?.status !== 416) {
110
+ throw new Error(`HTTP error! status: ${response?.status}`);
111
+ }
112
+
113
+ const contentLength = response.headers.get('Content-Length') as string;
114
+
115
+ if (!contentLength && file.downloaded > 0) return;
116
+
117
+ const restFileSize = parseInt(contentLength);
118
+ file.size = restFileSize + file.downloaded;
119
+
120
+ this.filterFileSize(file);
121
+
122
+ if (file.size > file.downloaded && response.body) {
123
+ const stream = Readable.fromWeb(response.body);
124
+ stream.setMaxListeners(20);
125
+ await this.fetchStream(file, stream, file.downloaded);
126
+ }
127
+ } catch (error) {
128
+ if (signal.aborted) {
129
+ if (signal.reason === 'FILE_SKIP') return;
130
+ }
131
+ if (retries > 0) {
132
+ if (/coomer|kemono/.test(file.url)) {
133
+ file.url = tryFixCoomerUrl(file.url, retries);
134
+ }
135
+ await sleep(1000);
136
+ return await this.downloadFile(file, retries - 1);
137
+ }
138
+ throw error;
139
+ }
140
+ }
141
+
142
+ async downloadFiles(): Promise<void> {
143
+ mkdir(this.filelist.dirPath as string);
144
+
145
+ this.subject.next({ type: 'FILES_DOWNLOADING_START' });
146
+
147
+ for (const file of this.filelist.files) {
148
+ file.active = true;
149
+
150
+ this.subject.next({ type: 'FILE_DOWNLOADING_START' });
151
+
152
+ await this.downloadFile(file);
153
+
154
+ file.active = false;
155
+
156
+ this.subject.next({ type: 'FILE_DOWNLOADING_END' });
157
+ }
158
+
159
+ this.subject.next({ type: 'FILES_DOWNLOADING_END' });
160
+ }
161
+ }
@@ -0,0 +1,113 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import logger from '../logger';
4
+ import type { MediaType } from '../types';
5
+ import { collectUniquesAndDuplicatesBy, removeDuplicatesBy } from '../utils/duplicates';
6
+ import { filterString } from '../utils/filters';
7
+ import { deleteFile, getFileHash, getFileSize, sanitizeFilename } from '../utils/io';
8
+ import { testMediaType } from '../utils/mediatypes';
9
+
10
+ export class CoomerFile {
11
+ public active = false;
12
+ public hash?: string;
13
+
14
+ constructor(
15
+ public name: string,
16
+ public url: string,
17
+ public filepath = '',
18
+ public size?: number,
19
+ public downloaded = 0,
20
+ public content?: string,
21
+ ) {}
22
+
23
+ public async getDownloadedSize() {
24
+ this.downloaded = await getFileSize(this.filepath as string);
25
+ return this;
26
+ }
27
+
28
+ public get textContent() {
29
+ const text = `${this.name || ''} ${this.content || ''}`.toLowerCase();
30
+ return text;
31
+ }
32
+
33
+ public static from(f: Pick<CoomerFile, 'name' | 'url'> & Partial<CoomerFile>) {
34
+ return new CoomerFile(f.name, f.url, f.filepath, f.size, f.downloaded, f.content);
35
+ }
36
+ }
37
+
38
+ export class CoomerFileList {
39
+ public dirPath?: string;
40
+ public dirName?: string;
41
+
42
+ constructor(public files: CoomerFile[] = []) {}
43
+
44
+ public setDirPath(dir: string, dirName?: string) {
45
+ dirName = dirName || (this.dirName as string);
46
+
47
+ if (dir === './') {
48
+ this.dirPath = path.resolve(dir, dirName);
49
+ } else {
50
+ this.dirPath = path.join(os.homedir(), path.join(dir, dirName));
51
+ }
52
+
53
+ this.files.forEach((file) => {
54
+ const safeName = sanitizeFilename(file.name) || file.name;
55
+ file.filepath = path.join(this.dirPath as string, safeName);
56
+ });
57
+
58
+ return this;
59
+ }
60
+
61
+ public filterByText(include: string, exclude: string) {
62
+ this.files = this.files.filter((f) => filterString(f.textContent, include, exclude));
63
+ return this;
64
+ }
65
+
66
+ public filterByMediaType(media?: string) {
67
+ if (media) {
68
+ this.files = this.files.filter((f) => testMediaType(f.name, media as MediaType));
69
+ }
70
+ return this;
71
+ }
72
+
73
+ public skip(n: number) {
74
+ this.files = this.files.slice(n);
75
+ return this;
76
+ }
77
+
78
+ public async calculateFileSizes() {
79
+ for (const file of this.files) {
80
+ await file.getDownloadedSize();
81
+ }
82
+ return this;
83
+ }
84
+
85
+ public getActiveFiles() {
86
+ return this.files.filter((f) => f.active);
87
+ }
88
+
89
+ public getDownloaded() {
90
+ return this.files.filter((f) => f.size && f.size <= f.downloaded);
91
+ }
92
+
93
+ public async removeDuplicatesByHash() {
94
+ for (const file of this.files) {
95
+ file.hash = await getFileHash(file.filepath);
96
+ }
97
+
98
+ const { duplicates } = collectUniquesAndDuplicatesBy(this.files, 'hash');
99
+
100
+ console.log({ duplicates });
101
+
102
+ logger.debug(`duplicates: ${JSON.stringify(duplicates)}`);
103
+
104
+ duplicates.forEach((f) => {
105
+ deleteFile(f.filepath);
106
+ });
107
+ }
108
+
109
+ public removeURLDuplicates() {
110
+ this.files = removeDuplicatesBy(this.files, 'url');
111
+ return this;
112
+ }
113
+ }
@@ -1 +1,16 @@
1
- export type MediaType = 'video' | 'image' | 'all';
1
+ export type MediaType = 'video' | 'image';
2
+
3
+ export type DownloaderSubjectSignal =
4
+ | 'FILES_DOWNLOADING_START'
5
+ | 'FILES_DOWNLOADING_END'
6
+ | 'FILE_DOWNLOADING_START'
7
+ | 'FILE_DOWNLOADING_END'
8
+ | 'CHUNK_DOWNLOADING_START'
9
+ | 'CHUNK_DOWNLOADING_UPDATE'
10
+ | 'CHUNK_DOWNLOADING_END';
11
+
12
+ export type AbortControllerSubject = 'FILE_SKIP' | 'TIMEOUT';
13
+
14
+ export type DownloaderSubject = {
15
+ type: DownloaderSubjectSignal;
16
+ };
@@ -0,0 +1,23 @@
1
+ export function collectUniquesAndDuplicatesBy<T extends {}, K extends keyof T>(
2
+ xs: T[],
3
+ k: K,
4
+ ): { uniques: T[]; duplicates: T[] } {
5
+ const seen = new Set<T[K]>();
6
+
7
+ return xs.reduce(
8
+ (acc, item) => {
9
+ if (seen.has(item[k])) {
10
+ acc.duplicates.push(item);
11
+ } else {
12
+ seen.add(item[k]);
13
+ acc.uniques.push(item);
14
+ }
15
+ return acc;
16
+ },
17
+ { uniques: [] as T[], duplicates: [] as T[] },
18
+ );
19
+ }
20
+
21
+ export function removeDuplicatesBy<T extends {}, K extends keyof T>(xs: T[], k: K) {
22
+ return [...new Map(xs.map((x) => [x[k], x])).values()];
23
+ }
@@ -1,17 +1,3 @@
1
- import type { MediaType } from '../types';
2
-
3
- export function isImage(name: string) {
4
- return /\.(jpg|jpeg|png|gif|bmp|tiff|webp|avif)$/i.test(name);
5
- }
6
-
7
- export function isVideo(name: string) {
8
- return /\.(mp4|m4v|avi|mov|mkv|webm|flv|wmv|mpeg|mpg|3gp)$/i.test(name);
9
- }
10
-
11
- export function testMediaType(name: string, type: MediaType) {
12
- return type === 'all' ? true : type === 'image' ? isImage(name) : isVideo(name);
13
- }
14
-
15
1
  function includesAllWords(str: string, words: string[]) {
16
2
  if (!words.length) return true;
17
3
  return words.every((w) => str.includes(w));
@@ -30,5 +16,19 @@ function parseQuery(query: string) {
30
16
  }
31
17
 
32
18
  export function filterString(text: string, include: string, exclude: string): boolean {
33
- return includesAllWords(text, parseQuery(include)) && includesNoWords(text, parseQuery(exclude));
19
+ return (
20
+ includesAllWords(text, parseQuery(include)) &&
21
+ includesNoWords(text, parseQuery(exclude))
22
+ );
23
+ }
24
+
25
+ export function parseSizeValue(s: string) {
26
+ if (!s) return NaN;
27
+ const m = s.match(/^([0-9]+(?:\.[0-9]+)?)(b|kb|mb|gb)?$/i);
28
+ if (!m) return NaN;
29
+ const val = parseFloat(m[1]);
30
+ const unit = (m[2] || 'b').toLowerCase();
31
+ const mult =
32
+ unit === 'kb' ? 1024 : unit === 'mb' ? 1024 ** 2 : unit === 'gb' ? 1024 ** 3 : 1;
33
+ return Math.floor(val * mult);
34
34
  }
package/src/utils/io.ts CHANGED
@@ -1,4 +1,7 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import fs from 'node:fs';
3
+ import { access, constants, unlink } from 'node:fs/promises';
4
+ import { pipeline } from 'node:stream/promises';
2
5
 
3
6
  export async function getFileSize(filepath: string) {
4
7
  let size = 0;
@@ -8,8 +11,30 @@ export async function getFileSize(filepath: string) {
8
11
  return size;
9
12
  }
10
13
 
14
+ export async function getFileHash(filepath: string) {
15
+ const hash = createHash('sha256');
16
+ const filestream = fs.createReadStream(filepath);
17
+ await pipeline(filestream, hash);
18
+ return hash.digest('hex');
19
+ }
20
+
11
21
  export function mkdir(filepath: string) {
12
22
  if (!fs.existsSync(filepath)) {
13
23
  fs.mkdirSync(filepath, { recursive: true });
14
24
  }
15
25
  }
26
+
27
+ export async function deleteFile(path: string) {
28
+ await access(path, constants.F_OK);
29
+ await unlink(path);
30
+ }
31
+
32
+ export function sanitizeFilename(name: string) {
33
+ if (!name) return name;
34
+
35
+ return name
36
+ .replace(/[<>:"/\\|?*\x00-\x1F]/g, '-') // Newlines (\r \n) are caught here
37
+ .replace(/\s+/g, ' ') // Turn tabs/multiple spaces into one space
38
+ .trim()
39
+ .replace(/[.]+$/, ''); // Remove trailing dots
40
+ }
@@ -0,0 +1,13 @@
1
+ import type { MediaType } from '../types';
2
+
3
+ export function isImage(name: string) {
4
+ return /\.(jpg|jpeg|png|gif|bmp|tiff|webp|avif)$/i.test(name);
5
+ }
6
+
7
+ export function isVideo(name: string) {
8
+ return /\.(mp4|m4v|avi|mov|mkv|webm|flv|wmv|mpeg|mpg|3gp)$/i.test(name);
9
+ }
10
+
11
+ export function testMediaType(name: string, type: MediaType) {
12
+ return type === 'image' ? isImage(name) : isVideo(name);
13
+ }
@@ -1,53 +1,3 @@
1
1
  export async function sleep(time: number) {
2
2
  return new Promise((resolve) => setTimeout(resolve, time));
3
3
  }
4
-
5
- type PromiseRetryCallback = (retries: number, error: Error) => void | { newRetries?: number };
6
-
7
- interface PromiseRetryOptions {
8
- retries?: number;
9
- callback?: PromiseRetryCallback;
10
- delay?: number;
11
- }
12
-
13
- export class PromiseRetry {
14
- private retries: number;
15
- private delay: number;
16
- private callback?: PromiseRetryCallback;
17
-
18
- constructor(options: PromiseRetryOptions) {
19
- this.retries = options.retries || 3;
20
- this.delay = options.delay || 1000;
21
- this.callback = options.callback;
22
- }
23
-
24
- async execute(fn: () => Promise<void>) {
25
- let retries = this.retries;
26
-
27
- while (true) {
28
- try {
29
- return await fn();
30
- } catch (error) {
31
- if (retries <= 0) {
32
- throw error;
33
- }
34
-
35
- if (this.callback) {
36
- const res = this.callback(retries, error as Error);
37
- if (res) {
38
- const { newRetries } = res;
39
- if (newRetries === 0) throw error;
40
- this.retries = newRetries || retries;
41
- }
42
- }
43
-
44
- await sleep(this.delay);
45
- retries--;
46
- }
47
- }
48
- }
49
-
50
- static create(options: PromiseRetryOptions) {
51
- return new PromiseRetry(options);
52
- }
53
- }
@@ -29,8 +29,8 @@ export function fetchWithGlobalHeader(url: string) {
29
29
  return fetch(url, { headers: requestHeaders });
30
30
  }
31
31
 
32
- export function fetchByteRange(url: string, downloadedSize: number) {
32
+ export function fetchByteRange(url: string, downloadedSize: number, signal?: AbortSignal) {
33
33
  const requestHeaders = new Headers(HeadersDefault);
34
34
  requestHeaders.set('Range', `bytes=${downloadedSize}-`);
35
- return fetch(url, { headers: requestHeaders });
35
+ return fetch(url, { headers: requestHeaders, signal });
36
36
  }
@@ -1,12 +1,3 @@
1
1
  export function b2mb(bytes: number) {
2
- return Number.parseFloat((bytes / 1048576).toFixed(2));
3
- }
4
-
5
- export function formatNameStdout(pathname: string) {
6
- const name = pathname.split('/').pop() || '';
7
- const consoleWidth = process.stdout.columns;
8
- const width = Math.max((consoleWidth / 2) | 0, 40);
9
- if (name.length < width) return name.trim();
10
- const result = `${name.slice(0, width - 15)} ... ${name.slice(-10)}`.replace(/ +/g, ' ');
11
- return result;
2
+ return (bytes / 1048576).toFixed(2);
12
3
  }
@@ -1,5 +1,8 @@
1
+ import type { Subject } from 'rxjs';
2
+ import type { AbortControllerSubject } from '../types';
3
+
1
4
  export class Timer {
2
- private timer: NodeJS.Timeout | undefined = undefined;
5
+ private timer: NodeJS.Timeout | undefined;
3
6
 
4
7
  constructor(
5
8
  private timeout = 10_000,
@@ -30,18 +33,17 @@ export class Timer {
30
33
  return this;
31
34
  }
32
35
 
33
- static withSignal(timeout?: number, message?: string) {
34
- const controller = new AbortController();
35
-
36
+ static withAbortController(
37
+ timeout: number,
38
+ abortControllerSubject: Subject<AbortControllerSubject>,
39
+ message: AbortControllerSubject = 'TIMEOUT',
40
+ ) {
36
41
  const callback = () => {
37
- controller.abort(message);
42
+ abortControllerSubject.next(message);
38
43
  };
39
44
 
40
45
  const timer = new Timer(timeout, callback).start();
41
46
 
42
- return {
43
- timer,
44
- signal: controller.signal,
45
- };
47
+ return { timer };
46
48
  }
47
49
  }
package/tsconfig.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
+ "jsx": "react",
3
4
  "target": "ESNext",
4
5
  "useDefineForClassFields": true,
5
6
  "module": "esnext",
@@ -9,7 +10,7 @@
9
10
  "moduleResolution": "bundler",
10
11
  "isolatedModules": true,
11
12
  "moduleDetection": "force",
12
-
13
+
13
14
  "declaration": true,
14
15
  "typeRoots": ["./dist/index.d.ts", "./src/types", "./node_modules/@types"],
15
16
  "outDir": "./dist",