diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..6dbf087 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,23 @@ +--- +name: Bug Report +about: New issues are for bug reports only. For everything else, use Discussions. +title: '' +labels: '' +assignees: '' + +--- + +### Expected behavior + + +### Actual behavior + + +### Steps to reproduce + + +### Environment + +* Version of sub-events: +* OS type (Linux/Windows/Mac): +* Version of Node.js: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f67559a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: ci + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node-version: [18.x, 20.x] + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - run: npm install + - run: npm run compile + - run: npm run lint + - run: npm test + - run: npm run build + - run: npm run doc diff --git a/README.md b/README.md index 7caac40..28d2292 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Strongly-Typed Events +[![Build Status](https://github.com/vitaly-t/sub-events/actions/workflows/ci.yml/badge.svg)](https://github.com/vitaly-t/sub-events/actions/workflows/ci.yml) + Lightweight, strongly-typed events, with monitored subscriptions. * Documentation: [API] + [WiKi]. diff --git a/package.json b/package.json index af5be8a..d408c56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sub-events", - "version": "1.9.0", + "version": "1.9.1", "description": "Lightweight, strongly-typed events, with monitored subscriptions.", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -11,7 +11,7 @@ "compile": "tsc -p src && tsc -p extras/src && tsc extras/deploy.ts && tsc -p test", "doc": "typedoc ./src/index.ts", "lint": "tslint --fix ./src/**/*.ts ./extras/src/*.ts ./test/**/*.ts", - "test": "nyc mocha -r ts-node/register test/**/*.spec.ts" + "test": "mocha --timeout 10000 --import=tsx test/**/*.spec.ts --exit" }, "files": [ "dist/src", @@ -28,7 +28,7 @@ }, "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" }, "bugs": { "url": "https://github.com/vitaly-t/sub-events/issues" @@ -41,20 +41,19 @@ "typescript" ], "devDependencies": { - "@types/chai": "4.3.3", - "@types/chai-spies": "1.0.3", - "@types/mocha": "9.1.1", - "@types/node": "18.7.22", + "@types/chai": "5.0.0", + "@types/chai-spies": "1.0.6", + "@types/mocha": "10.0.8", + "@types/node": "22.7.4", "browserify": "17.0.0", - "chai": "4.3.6", - "chai-spies": "1.0.0", - "mocha": "10.0.0", + "chai": "5.1.1", + "chai-spies": "1.1.0", + "mocha": "10.7.3", "mocha-lcov-reporter": "1.3.0", - "nyc": "15.1.0", - "ts-node": "10.9.1", - "tslib": "2.4.0", + "tslib": "2.7.0", "tslint": "6.1.3", - "typedoc": "0.20.37", - "typescript": "4.2.4" + "tsx": "4.19.1", + "typedoc": "0.26.7", + "typescript": "5.6.2" } } diff --git a/src/consumer.ts b/src/consumer.ts index 1b5c83e..7f2bb08 100644 --- a/src/consumer.ts +++ b/src/consumer.ts @@ -10,12 +10,10 @@ import {Private} from './utils'; const pp = new Private>(); /** - * ### class EventConsumer\ = SubEvent\> + * Encapsulates an event object, in order to hide its methods {@link SubEvent.emit} and {@link SubEvent.cancelAll}, + * so the event consumer can only receive the event, but cannot emit it, or cancel other subscriptions. * - * Encapsulates an event object, in order to hide its methods [[emit]] and [[cancelAll]], so the event - * consumer can only receive the event, but cannot emit it, or cancel other subscriptions. - * - * It is a non-extendable class, with the same signature as [[SubEvent]], minus methods [[emit]] and [[cancelAll]]. + * It is a non-extendable class, with the same signature as {@link SubEvent}, minus {@link SubEvent.emit emit} and {@link SubEvent.cancelAll cancelAll}. * * ```ts * // Example of using EventConsumer inside a component. @@ -53,42 +51,42 @@ export class EventConsumer = SubEvent> { } /** - * Forwards into [[SubEvent.count]] of the contained event. + * Forwards into {@link SubEvent.count} of the contained event. */ get count(): number { return pp.get(this).count; } /** - * Forwards into [[SubEvent.maxSubs]] of the contained event. + * Forwards into {@link SubEvent.maxSubs} of the contained event. */ get maxSubs(): number { return pp.get(this).maxSubs; } /** - * Forwards into [[SubEvent.subscribe]] of the contained event. + * Forwards into {@link SubEvent.subscribe} of the contained event. */ subscribe(cb: SubFunction, options?: ISubOptions): Subscription { return pp.get(this).subscribe(cb, options); } /** - * Forwards into [[SubEvent.once]] of the contained event. + * Forwards into {@link SubEvent.once} of the contained event. */ once(cb: SubFunction, options?: ISubOptions): Subscription { return pp.get(this).once(cb, options); } /** - * Forwards into [[SubEvent.toPromise]] of the contained event. + * Forwards into {@link SubEvent.toPromise} of the contained event. */ toPromise(options?: { name?: string, timeout?: number }): Promise { return pp.get(this).toPromise(options); } /** - * Forwards into [[SubEvent.getStat]] of the contained event. + * Forwards into {@link SubEvent.getStat} of the contained event. */ getStat(options?: { minUse?: number }): ISubStat { return pp.get(this).getStat(options); diff --git a/src/count.ts b/src/count.ts index 8eda0e3..4722e58 100644 --- a/src/count.ts +++ b/src/count.ts @@ -1,7 +1,7 @@ import {IEmitOptions, IEventOptions, ISubscriber, SubEvent} from './event'; /** - * Represents a change in the number of subscriptions, as used with [[onCount]] event. + * Represents a change in the number of subscriptions, as used with {@link SubEventCount.onCount} event. */ export interface ISubCountChange { /** @@ -16,19 +16,17 @@ export interface ISubCountChange { } /** - * Constructor options for [[SubEventCount]] class. + * Constructor options for {@link SubEventCount} class. */ export interface ICountOptions extends IEventOptions { /** - * Emit options for event [[onCount]]. + * Emit options for event {@link SubEventCount.onCount}. */ emitOptions?: IEmitOptions; } /** - * ### class SubEventCount\ extends SubEvent\ - * - * Extends [[SubEvent]] with event [[onCount]], to observe the number of subscriptions. + * Extends {@link SubEvent} with event {@link onCount}, to observe the number of subscriptions. */ export class SubEventCount extends SubEvent { @@ -44,7 +42,6 @@ export class SubEventCount extends SubEvent { readonly onCount: SubEvent = new SubEvent(); /** - * @constructor * Event constructor. * * @param options @@ -59,13 +56,13 @@ export class SubEventCount extends SubEvent { /** * Cancels all existing subscriptions for the event. * - * It overrides the base implementation, to trigger event [[onCount]] + * It overrides the base implementation, to trigger event {@link onCount} * when there was at least one subscription. * * @returns * Number of subscriptions cancelled. * - * @see [[cancel]] + * @see {@link Subscription.cancel} */ public cancelAll(): number { const prevCount = this.count; @@ -77,8 +74,9 @@ export class SubEventCount extends SubEvent { } /** - * Overrides base implementation, to trigger event [[onCount]] during + * Overrides base implementation, to trigger event {@link onCount} during * `subscribe` and `cancel` calls. + * * @hidden */ protected _createCancel(sub: ISubscriber): () => void { diff --git a/src/event.ts b/src/event.ts index 6af4afe..73f722e 100644 --- a/src/event.ts +++ b/src/event.ts @@ -2,7 +2,7 @@ import {Subscription} from './sub'; import {EventConsumer} from './consumer'; /** - * Schedule for emitting / broadcasting data to subscribers, to be used by method [[emit]]. + * Schedule for emitting / broadcasting data to subscribers, to be used by method {@link SubEvent.emit}. * It represents a concurrency strategy for delivering event to subscribers. */ export enum EmitSchedule { @@ -32,7 +32,7 @@ export enum EmitSchedule { } /** - * Options to be used with method [[emit]]. + * Options to be used with method {@link SubEvent.emit}. */ export interface IEmitOptions { /** @@ -44,7 +44,7 @@ export interface IEmitOptions { * Callback for catching all unhandled errors from subscribers, * from both synchronous and asynchronous subscription functions. * - * ```js + * ```ts * (err: any, name?: string) => void; * ``` * @@ -52,7 +52,7 @@ export interface IEmitOptions { * `err`: The error that was thrown or rejected. * * @param name - * `name`: The subscription `name`, if set during [[subscribe]] call. + * `name`: The subscription `name`, if set during {@link SubEvent.subscribe} call. */ onError?: (err: any, name?: string) => void; @@ -60,7 +60,7 @@ export interface IEmitOptions { * Notification callback of when the last recipient has received the data. * Note that asynchronous subscribers may still be processing the data at this point. * - * ```js + * ```ts * (count: number) => void; * ``` * @@ -71,8 +71,8 @@ export interface IEmitOptions { } /** - * Subscription Context Interface, as used with [[onSubscribe]] and [[onCancel]] - * notification options that can be set during [[SubEvent]] construction. + * Subscription Context Interface, as used with {@link IEventOptions.onSubscribe} and {@link IEventOptions.onCancel} + * notification options that can be set during {@link SubEvent} construction. */ export interface ISubContext { /** @@ -81,7 +81,7 @@ export interface ISubContext { readonly event: SubEvent; /** - * Subscription name, if one was specified with method [[subscribe]]. + * Subscription name, if one was specified with method {@link SubEvent.subscribe}. */ readonly name?: string; @@ -93,14 +93,14 @@ export interface ISubContext { } /** - * Constructor options for [[SubEvent]] class. + * Constructor options for {@link SubEvent} class. */ export interface IEventOptions { /** * Maximum number of subscribers that can receive events. * Default is 0, meaning `no limit applies`. * - * Newer subscriptions outside of the maximum quota will start + * Newer subscriptions outside the maximum quota will start * receiving events when the older subscriptions get cancelled. */ maxSubs?: number; @@ -108,12 +108,12 @@ export interface IEventOptions { /** * Notification of a new subscriber being registered. * - * ```js + * ```ts * (ctx: ISubContext) => void; * ``` * * @param ctx - * `ctx`: [[ISubContext]] - Subscription Context. + * `ctx`: {@link ISubContext} - Subscription Context. */ onSubscribe?: (ctx: ISubContext) => void; @@ -125,22 +125,22 @@ export interface IEventOptions { * ``` * * @param ctx - * `ctx`: [[ISubContext]] - Subscription Context. + * `ctx`: {@link ISubContext} - Subscription Context. */ onCancel?: (ctx: ISubContext) => void; } /** - * Options that can be passed into method [[subscribe]]. + * Options that can be passed into method {@link SubEvent.subscribe}. */ export interface ISubOptions { /** * Unique subscription name. It helps with diagnosing subscription leaks, - * via method [[getStat]], and provides additional details during error handling. + * via method {@link SubEvent.getStat}, and provides additional details during error handling. * The name should help identify place in the code where the subscription was created. * - * @see [[getStat]] + * @see {@link SubEvent.getStat} */ name?: string; @@ -151,6 +151,7 @@ export interface ISubOptions { * ```ts * event.subscribe(func.bind(this)) * ``` + * * With this option you can also do it this way: * ```ts * event.subscribe(func, {thisArg: this}) @@ -160,7 +161,7 @@ export interface ISubOptions { /** * Subscription-cancel callback, to be notified on subscription explicit - * [[cancel]] call, or when cancelled implicitly via [[cancelAll]]. + * {@link Subscription.cancel} call, or when cancelled implicitly via {@link SubEvent.cancelAll}. * * This is mostly for internal usage, and has no protection against * errors, should the handler throw any. @@ -169,13 +170,13 @@ export interface ISubOptions { } /** - * Subscriptions statistics, as returned by method [[getStat]]. + * Subscriptions statistics, as returned by method {@link SubEvent.getStat}. */ export interface ISubStat { /** * Map of subscription names to their usage counters. It consists of only - * subscriptions for which option `name` was set when calling [[subscribe]]. + * subscriptions for which option `name` was set when calling {@link SubEvent.subscribe}. */ named: { [name: string]: number }; @@ -208,13 +209,9 @@ export interface ISubscriber extends ISubContext { } /** - * ### class SubEvent\ - * - * @class SubEvent - * @description * Core class, implementing event subscription + emitting the event. * - * @see [[subscribe]], [[emit]] + * @see {@link subscribe}, {@link emit} */ export class SubEvent { @@ -230,7 +227,6 @@ export class SubEvent { protected _subs: ISubscriber[] = []; /** - * @constructor * Event constructor. * * @param options @@ -244,7 +240,7 @@ export class SubEvent { } /** - * Returns a new [[EventConsumer]] for the event, which physically hides methods [[emit]] and [[cancelAll]]. + * Returns a new {@link EventConsumer} for the event, which physically hides methods {@link SubEvent.emit} and {@link SubEvent.cancelAll}. * * This method simplifies creation of a receive-only event object representation. * @@ -264,10 +260,10 @@ export class SubEvent { /** * Subscribes to the event. * - * When subscription is no longer needed, method [[cancel]] should be called on the + * When subscription is no longer needed, method {@link Subscription.cancel} should be called on the * returned object, to avoid performance degradation caused by abandoned subscribers. * - * Method [[getStat]] can help with diagnosing leaked subscriptions. + * Method {@link SubEvent.getStat} can help with diagnosing leaked subscriptions. * * @param cb * Event notification callback function. @@ -278,7 +274,7 @@ export class SubEvent { * @returns * Object for cancelling the subscription safely. * - * @see [[once]] + * @see {@link once} */ public subscribe(cb: SubFunction, options?: ISubOptions): Subscription { if (typeof (options ?? {}) !== 'object') { @@ -304,7 +300,7 @@ export class SubEvent { /** * Subscribes to receive just one event, and cancel the subscription immediately. * - * You may still want to call [[cancel]] on the returned [[Subscription]] object, + * You may still want to call {@link Subscription.cancel} on the returned object, * if you suddenly need to prevent the first event, or to avoid dead once-off * subscriptions that never received their event, and thus were not cancelled. * @@ -317,7 +313,7 @@ export class SubEvent { * @returns * Object for cancelling the subscription safely. * - * @see [[toPromise]] + * @see {@link toPromise} */ public once(cb: SubFunction, options?: ISubOptions): Subscription { const sub = this.subscribe((data: T) => { @@ -328,7 +324,7 @@ export class SubEvent { } /** - * Broadcasts data to all subscribers, according to the emit schedule, + * Broadcasts data to all subscribers, according to the `emit` schedule, * which is synchronous by default. * * @param data @@ -383,10 +379,10 @@ export class SubEvent { * Maximum number of subscribers that can receive events. * Default is 0, meaning `no limit applies`. * - * Newer subscriptions outside of the maximum quota will start + * Newer subscriptions outside the maximum quota will start * receiving events when the older subscriptions get cancelled. * - * It can only be set with the [[constructor]]. + * It can only be set with the constructor. */ public get maxSubs(): number { return this.options.maxSubs ?? 0; @@ -395,7 +391,7 @@ export class SubEvent { /** * Retrieves subscriptions statistics, to help with diagnosing subscription leaks. * - * For this method to be useful, you need to set option `name` when calling [[subscribe]]. + * For this method to be useful, you need to set option `name` when calling {@link SubEvent.subscribe}. * * See also: {@link https://github.com/vitaly-t/sub-events/wiki/Diagnostics Diagnostics} * @@ -403,9 +399,9 @@ export class SubEvent { * Statistics Options: * * - `minUse: number` - Minimum subscription usage/count to be included into the list of named - * subscriptions. If subscription is used less times, it will be excluded from the `named` list. + * subscriptions. If subscription is used fewer times, it will be excluded from the `named` list. * - * @see [[ISubStat]] + * @see {@link ISubStat} */ public getStat(options?: { minUse?: number }): ISubStat { const stat: ISubStat = {named: {}, unnamed: 0}; @@ -435,7 +431,7 @@ export class SubEvent { * Cancels all existing subscriptions for the event. * * This is a convenience method for some special cases, when you want to cancel all subscriptions - * for the event at once. Usually, subscribers just call [[cancel]] when they want to cancel their + * for the event at once. Usually, subscribers just call {@link Subscription cancel} when they want to cancel their * own subscription. * * This method will always offer much better performance than cancelling each subscription individually, @@ -444,7 +440,7 @@ export class SubEvent { * @returns * Number of subscriptions cancelled. * - * @see [[cancel]] + * @see {@link Subscription.cancel} */ public cancelAll(): number { const onCancel = typeof this.options.onCancel === 'function' && this.options.onCancel; @@ -483,7 +479,7 @@ export class SubEvent { * * The returned promise can reject in two cases: * - when the timeout has been reached (if set via option `timeout`), it rejects with `Event timed out` error; - * - when [[cancelAll]] is called on the event object, it rejects with `Event cancelled` error. + * - when {@link cancelAll} is called on the event object, it rejects with `Event cancelled` error. * * Note that if you use this method consecutively, you can miss events in between, * because the subscription is auto-cancelled after receiving the first event. @@ -491,13 +487,13 @@ export class SubEvent { * @param options * Subscription Options: * - * - `name` - for the internal subscription name. See `name` in [[ISubOptions]]. + * - `name` - for the internal subscription name. See `name` in {@link ISubOptions}. * In this context, it is also included within any rejection error. * * - `timeout` - sets timeout in ms (when `timeout` >= 0), to auto-reject with * `Event timed out` error. * - * @see [[once]] + * @see {@link once} */ public toPromise(options?: { name?: string, timeout?: number }): Promise { if (typeof (options ?? {}) !== 'object') { @@ -535,8 +531,8 @@ export class SubEvent { /** * Gets all recipients that must receive data. * - * It returns a copy of subscribers array for safe iteration, while applying the - * maximum limit when it is set with the [[maxSubs]] option. + * It returns a copy of subscribers' array for safe iteration, while applying the + * maximum limit when it is set with the {@link IEventOptions.maxSubs} option. * * @hidden */ @@ -546,14 +542,14 @@ export class SubEvent { } /** - * Creates unsubscribe callback function for the [[Subscription]] class. + * Creates unsubscribe callback function for the {@link Subscription} class. * @hidden * * @param sub * Subscriber details. * * @returns - * Function that implements the [[unsubscribe]] request. + * Function that implements the `unsubscribe` request. */ protected _createCancel(sub: ISubscriber): () => void { return () => { diff --git a/src/sub.ts b/src/sub.ts index 8ca6a93..8e5ba05 100644 --- a/src/sub.ts +++ b/src/sub.ts @@ -1,9 +1,7 @@ /** - * ### class Subscription - * * Represents an event subscription, and a safe way to cancel it. * - * @see [[cancel]] + * @see {@link cancel} */ export class Subscription { /** @@ -12,7 +10,7 @@ export class Subscription { private _cancel: null | (() => void); /** - * Subscription's `name` option, if it was set with method [[subscribe]]. + * Subscription's `name` option, if it was set with method {@link SubEvent.subscribe}. */ readonly name?: string; @@ -32,7 +30,7 @@ export class Subscription { /** * Indicates whether the subscription is live / active. * - * It can be useful to subscribers when [[cancelAll]] is used without their knowledge. + * It can be useful to subscribers when {@link SubEvent.cancelAll} is used without their knowledge. */ public get live(): boolean { return !!this._cancel; @@ -41,13 +39,13 @@ export class Subscription { /** * Cancels the live subscription. The subscriber won't receive any more events. * - * It also sets flag [[live]] to `false`. + * It also sets flag {@link live} to `false`. * * @returns * - `true` - subscription has been successfully cancelled * - `false` - nothing happened, as subscription wasn't live * - * @see [[cancelAll]] + * @see {@link SubEvent.cancelAll} */ public cancel(): boolean { if (this._cancel) { diff --git a/test/count.spec.ts b/test/count.spec.ts index cdeb065..d3aed97 100644 --- a/test/count.spec.ts +++ b/test/count.spec.ts @@ -1,4 +1,5 @@ import {chai, expect} from './'; + import {EmitSchedule, ISubCountChange, SubEventCount} from '../src'; const dummy = () => { diff --git a/test/index.ts b/test/index.ts index 96b9588..d078d0e 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,11 +1,11 @@ -import chai from 'chai'; +import * as ch from 'chai'; import spies from 'chai-spies'; import {describe} from 'mocha'; -chai.use(spies); +const chai = ch.use(spies); -const expect = chai.expect; -const should = chai.should(); +const expect = ch.expect; +const should = ch.should(); const dummy = () => { }; diff --git a/typedoc.css b/typedoc.css new file mode 100644 index 0000000..c41423b --- /dev/null +++ b/typedoc.css @@ -0,0 +1,11 @@ +p > code { + padding: 4px 8px; + border: 1px solid var(--color-accent); + border-radius: 5px; +} + +li > code { + padding: 2px 6px; + border: 1px solid var(--color-accent); + border-radius: 5px; +} diff --git a/typedoc.json b/typedoc.json index 0f2be8b..8080510 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,12 +1,16 @@ { - "name": "SUB-EVENTS v1.9", - "readme": "README.md", - "out": "docs", - "theme": "minimal", + "$schema": "https://typedoc.org/schema.json", + "name": "SUB-EVENTS v1.9.1", + "out": "./docs", "excludeExternals": true, "excludePrivate": true, - "excludeNotDocumented": true, - "disableOutputCheck": true, + "cleanOutputDir": false, + "entryPoints": [ + "src/index.ts" + ], + "entryPointStrategy": "resolve", + "hideGenerator": true, + "customCss": "./typedoc.css", "exclude": [ "./extras/**/*.ts", "./test/**/*.ts"