diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8b4c83b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..70c6dff --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + pull_request: + branches: + - main + - develop + - 'release/v*.*.*' + push: + branches: + - main + - develop + - 'release/v*.*.*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Run checks + run: npm run verify + + - name: Build project + run: npm run build diff --git a/.husky/pre-push b/.husky/pre-push index 7b86ba1..73361f0 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,3 +1,3 @@ -npm run fix +npm run format npm run verify npx lint-staged \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1281c45..2355a44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog -## v1.0.0 +## v2.0.0 (March 4th, 2026) + +- `useReusableAborter` **hook** - hook wrapper over the `ReusableAborter` class +- Increases the `saborter` version to 2 majors + +## v1.0.0 (February 4th, 2026) ### New Features diff --git a/assets/logo.png b/assets/logo.png index 51544f5..72de979 100644 Binary files a/assets/logo.png and b/assets/logo.png differ diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..aa74856 --- /dev/null +++ b/cspell.json @@ -0,0 +1,36 @@ +{ + "version": "0.2", + "language": "en,ru", + "words": ["Saborter", "saborter", "Laptev", "Vladislav", "tgz", "Сalls"], + "flagWords": [], + "ignorePaths": [ + "node_modules/**", + "dist/**", + "build/**", + ".git/**", + "coverage/**", + "*.log", + "package-lock.json", + "./**/*.test.ts", + "./cspell.json", + "./integrations" + ], + "dictionaries": ["typescript", "node", "softwareTerms", "en_US", "ru_RU"], + "useGitignore": true, + "patterns": [ + { + "name": "markdown-links", + "pattern": "\\(.*\\)", + "description": "Ignore URLs in Markdown" + }, + { + "name": "html-entities", + "pattern": "&\\w+;", + "description": "Ignore HTML entities" + } + ], + "ignoreRegExpList": ["/\\[.*\\]\\(.*\\)/g", "/&[a-z]+;/g", "/0x[a-fA-F0-9]+/g", "/\\$[^{][\\w.]+/g"], + "caseSensitive": false, + "allowCompoundWords": true, + "minWordLength": 3 +} diff --git a/package-lock.json b/package-lock.json index 37ed0dc..4cdce54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "prettier": "^3.7.4", "react": "^17.0.2", "rimraf": "^6.1.2", - "saborter": "^1.4.2", + "saborter": "^2.0.1", "ts-node": "^10.9.2", "typescript": "^5.9.3", "vite": "^7.3.0", @@ -36,7 +36,7 @@ }, "peerDependencies": { "react": ">=17.0.0 <=20.0.0", - "saborter": "^1.4.2" + "saborter": "^2.0.1" } }, "node_modules/@babel/code-frame": { @@ -6883,9 +6883,9 @@ } }, "node_modules/saborter": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/saborter/-/saborter-1.4.2.tgz", - "integrity": "sha512-+z+ixvBB8cHfmh19hjMVsHu+fKzhYjrFw/RwNkXUtwLkWIm2r7vxlNetzWQlDUUZPakG6lZOKsHjbeosmT9pIA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/saborter/-/saborter-2.0.1.tgz", + "integrity": "sha512-S8n1PcieQP+yPDyIl/PnMUOOF7pM5x+VhO/JDEoGsVmq8YHsCSMsY8dIWuD6uH22n4NxTg0RfUhyn5+N5LxwkA==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index e0ad870..ccddec6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@saborter/react", - "version": "1.0.0", + "version": "2.0.0", "description": "A library for canceling asynchronous requests with React integration", "type": "module", "publishConfig": { @@ -47,9 +47,9 @@ "verify:prettier": "npx prettier . --check", "verify:eslint": "npx eslint \"./**/*.{js,jsx,ts,tsx}\" --max-warnings=0", "verify": "npm run typecheck && npm run verify:prettier && npm run verify:eslint", - "fix:eslint": "npx eslint \"./**/*.{js,jsx,ts,tsx}\" --fix", - "fix:prettier": "npx prettier --write .", - "fix": "npm run fix:eslint && npm run fix:prettier", + "format:eslint": "npx eslint \"./**/*.{js,jsx,ts,tsx}\" --fix", + "format:prettier": "npx prettier --write .", + "format": "npm run format:eslint && npm run format:prettier", "prepare": "husky" }, "devDependencies": { @@ -71,7 +71,7 @@ "prettier": "^3.7.4", "react": "^17.0.2", "rimraf": "^6.1.2", - "saborter": "^1.4.2", + "saborter": "^2.0.1", "ts-node": "^10.9.2", "typescript": "^5.9.3", "vite": "^7.3.0", @@ -80,14 +80,14 @@ }, "peerDependencies": { "react": ">=17.0.0 <=20.0.0", - "saborter": "^1.4.2" + "saborter": "^2.0.1" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ - "npm run fix:prettier" + "npm run format:prettier" ], "*.{json,md}": [ - "npm run fix:prettier" + "npm run format:prettier" ] } } diff --git a/readme.md b/readme.md index 1319873..9eea17b 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,17 @@ ![Logo](./assets/logo.png) -[![Npm package](https://img.shields.io/npm/v/@saborter/react?color=red&label=npm%20package)](https://www.npmjs.com/package/@saborter/react) -[![License](https://img.shields.io/badge/license-MIT-blue)](./LICENSE) -[![Github](https://img.shields.io/badge/repository-github-color)](https://github.com/TENSIILE/@saborter/react) +

+ + + + + + + + + + +

A library for canceling asynchronous requests that combines the `Saborter` library and `React`. @@ -126,6 +135,63 @@ const { requestState } = useAborter(); console.log(requestState); // 'cancelled' / 'pending' / 'fulfilled' / 'rejected' / 'aborted' ``` +### `useReusableAborter` + +#### Props + +```typescript +// The type can be found in `saborter/types` +const reusableAborter = new useReusableAborter(props?: ReusableAborterProps); +``` + +#### Props Parameters + +| Parameter | Type | Description | Required | +| --------- | ---------------------- | ------------------------------------- | -------- | +| `props` | `ReusableAborterProps` | ReusableAborter configuration options | No | + +**ReusableAborterProps:** + +```typescript +{ + /** + * Determines which listeners are carried over when the abort signal is reset. + * - If `true`, all listeners (both `onabort` and event listeners) are preserved. + * - If `false`, no listeners are preserved. + * - If an object, specific listener types can be enabled/disabled individually. + */ + attractListeners?: boolean | AttractListeners; +} +``` + +#### Properties + +`signal: AbortSignal` + +Returns the `AbortSignal` associated with the current controller. + +```javascript +const reusableAborter = useReusableAborter(); + +// Using signal in the request +fetch('/api/data', { + signal: reusableAborter.signal +}); +``` + +#### Methods + +`abort(reason?): void` + +**Parameters:** + +- `reason?: any` - the reason for aborting the request. + +Immediately cancels the currently executing request. + +> [!NOTE] +> Can be called multiple times. Each call will restore the `signal`, and the `aborted` property will always be `false`. + ## 🎯 Usage Examples ### Basic Usage @@ -163,7 +229,7 @@ const Component = () => { }; ``` -### The `AbortError` `initiator` changed while unmounting the component. +### The `AbortError` `initiator` changed while unmounting the component ```javascript import { AbortError } from 'saborter'; @@ -184,6 +250,39 @@ const Component = () => { }; ``` +### Using `useReusableAborter` + +```typescript +const aborter = new useReusableAborter(); + +// Get the current signal +const signal = aborter.signal; + +// Attach listeners +signal.addEventListener('abort', () => console.log('Listener 1')); +signal.addEventListener('abort', () => console.log('Listener 2'), { once: true }); // won't be recovered + +// Set onabort handler +signal.onabort = () => console.log('Onabort handler'); + +// First abort +aborter.abort('First reason'); +// Output: +// Listener 1 +// Listener 2 (once) +// Onabort handler + +// The signal is now a fresh one, but the non‑once listeners and onabort are reattached +signal.addEventListener('abort', () => console.log('Listener 3')); // new listener, will survive next abort + +// Second abort +aborter.abort('Second reason'); +// Output: +// Listener 1 +// Onabort handler +// Listener 3 +``` + ## 📋 License MIT License - see [LICENSE](./LICENSE) for details. diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 135fb70..8ca50fb 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,2 @@ export * from './use-aborter'; +export * from './use-reusable-aborter'; diff --git a/src/hooks/use-aborter/use-aborter.ts b/src/hooks/use-aborter/use-aborter.ts index fa92c9e..92a45e9 100644 --- a/src/hooks/use-aborter/use-aborter.ts +++ b/src/hooks/use-aborter/use-aborter.ts @@ -1,5 +1,8 @@ import { useRef, useEffect, useState } from 'react'; -import { Aborter, RequestState, AbortError } from 'saborter'; +import { Aborter } from 'saborter'; +import { AbortError } from 'saborter/errors'; +import { RequestState } from 'saborter/types'; +import { dispose as disposeFn } from 'saborter/lib'; import * as Shared from '../../shared'; import * as Constants from './use-aborter.constants'; import * as Types from './use-aborter.types'; @@ -7,7 +10,7 @@ import * as Types from './use-aborter.types'; export const useAborter = (props: Types.UseAborterProps = {}): Types.UseAborterResult => { const { onAbort, onStateChange, dispose = true } = props; - const aborterRef = useRef(new Aborter({ onAbort, onStateChange })); + const aborterRef = useRef(new Aborter({ onAbort: onAbort as any, onStateChange })); const [requestState, setRequestState] = useState(null); const isDisposeEnabledRef = Shared.Hooks.useMutableRef(dispose); @@ -27,7 +30,7 @@ export const useAborter = (props: Types.UseAborterProps = {}): Types.UseAborterR }) ); if (isDisposeEnabledCurrent) { - currentAborter.dispose(); + disposeFn(currentAborter); } }; }, [isDisposeEnabledRef]); diff --git a/src/hooks/use-aborter/use-aborter.types.ts b/src/hooks/use-aborter/use-aborter.types.ts index d34b02a..87e0d13 100644 --- a/src/hooks/use-aborter/use-aborter.types.ts +++ b/src/hooks/use-aborter/use-aborter.types.ts @@ -1,4 +1,5 @@ -import { Aborter, OnAbortCallback, OnStateChangeCallback, RequestState, AbortInitiator as Initiator } from 'saborter'; +import { Aborter } from 'saborter'; +import { OnAbortCallback, OnStateChangeCallback, RequestState, AbortInitiator as Initiator } from 'saborter/types'; import { ABORTABLE_UNMOUNTED_INITIATOR } from './use-aborter.constants'; export interface UseAborterResult { diff --git a/src/hooks/use-reusable-aborter/index.ts b/src/hooks/use-reusable-aborter/index.ts new file mode 100644 index 0000000..915a57f --- /dev/null +++ b/src/hooks/use-reusable-aborter/index.ts @@ -0,0 +1 @@ +export * from './use-reusable-aborter'; diff --git a/src/hooks/use-reusable-aborter/use-reusable-aborter.ts b/src/hooks/use-reusable-aborter/use-reusable-aborter.ts new file mode 100644 index 0000000..6678684 --- /dev/null +++ b/src/hooks/use-reusable-aborter/use-reusable-aborter.ts @@ -0,0 +1,9 @@ +import { useRef } from 'react'; +import { ReusableAborter } from 'saborter'; +import { ReusableAborterProps } from 'saborter/types'; + +export const useReusableAborter = (props: ReusableAborterProps = {}) => { + const reusableAborter = useRef(new ReusableAborter(props)); + + return reusableAborter.current; +};