From c011352a3f3d9a1bb70585a5e25bfba99320919f Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Mon, 18 Aug 2025 21:19:01 -0500 Subject: [PATCH 1/6] Add a count method --- src/createEmitter.ts | 21 +++++++++++++++++++++ tests/events.spec-d.ts | 29 +++++++++++++++++++++++++++++ tests/events.spec.ts | 18 ++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/src/createEmitter.ts b/src/createEmitter.ts index b606740..583495c 100644 --- a/src/createEmitter.ts +++ b/src/createEmitter.ts @@ -149,6 +149,26 @@ export function createEmitter(options?: EmitterOp onEvent(event, payload!) } + type CountOptions = { + global?: boolean + } + + function count(): number + function count(event: E, options?: CountOptions): number + function count(event?: E, { global = false }: CountOptions = {}): number { + if(event) { + const eventHandlers = handlers.get(event)?.size ?? 0 + + if(global) { + return eventHandlers + globalHandlers.size + } + + return eventHandlers + } + + return globalHandlers.size + } + function clear(): void { handlers.clear() globalHandlers.clear() @@ -210,6 +230,7 @@ export function createEmitter(options?: EmitterOp next, emit, clear, + count, setOptions, } } diff --git a/tests/events.spec-d.ts b/tests/events.spec-d.ts index 7388f55..0565dd2 100644 --- a/tests/events.spec-d.ts +++ b/tests/events.spec-d.ts @@ -198,3 +198,32 @@ describe('emitter.setOptions', () => { expectTypeOf().toEqualTypeOf() }) }) + +describe('emitter.count', () => { + test('has the correct return type', () => { + const emitter = createEmitter() + const response = emitter.count() + + type Source = typeof response + type Expected = number + + expectTypeOf().toEqualTypeOf() + }) + + test('accepts only valid event names', () => { + const emitter = createEmitter() + + emitter.count('ping') + emitter.count('hello') + emitter.count('user') + + // @ts-expect-error - Invalid event name + emitter.count('invalid') + }) + + test('accepts a global option', () => { + const emitter = createEmitter() + + emitter.count('ping', { global: true }) + }) +}) diff --git a/tests/events.spec.ts b/tests/events.spec.ts index bd80122..308659e 100644 --- a/tests/events.spec.ts +++ b/tests/events.spec.ts @@ -372,4 +372,22 @@ test('next without event returns the global event payload', async () => { kind: 'hello', payload: 'world', }) +}) + +describe('emitter.count', () => { + test('returns the correct number of handlers', () => { + const emitter = createEmitter<{ hello: void, goodbye: void }>() + + emitter.on('hello', vi.fn()) + emitter.on('goodbye', vi.fn()) + emitter.on(vi.fn()) + + expect(emitter.count()).toBe(1) + expect(emitter.count('hello')).toBe(1) + expect(emitter.count('goodbye')).toBe(1) + expect(emitter.count('hello', { global: true })).toBe(2) + expect(emitter.count('goodbye', { global: true })).toBe(2) + expect(emitter.count('hello', { global: false })).toBe(1) + expect(emitter.count('goodbye', { global: false })).toBe(1) + }) }) \ No newline at end of file From 52c366a790b542ce8c766822c9412e513cef5c33 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Mon, 18 Aug 2025 21:20:37 -0500 Subject: [PATCH 2/6] Add method to docs --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index c3bd1bd..b87301a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,7 +19,7 @@ features: - title: Type safety details: Type safe events and payload - title: Useful API - details: Includes on, off, once, next, emit, and clear + details: Includes on, off, once, next, emit, count, and clear - title: Support Global Handlers details: Setup handlers that run on all events --- From 7023fa909b72e97d82c998c1963202526009c485 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Mon, 18 Aug 2025 22:00:03 -0500 Subject: [PATCH 3/6] Refresh the docs --- docs/.vitepress/config.ts | 8 +- docs/README.md | 49 +++++++++ docs/additional-details.md | 116 -------------------- docs/broadcasting-events.md | 65 ++++++++++++ docs/getting-started.md | 91 ++++++++++++++-- docs/index.md | 25 +++-- docs/methods.md | 205 ++++++++++++++++++++++++++++++++++++ 7 files changed, 420 insertions(+), 139 deletions(-) create mode 100644 docs/README.md delete mode 100644 docs/additional-details.md create mode 100644 docs/broadcasting-events.md create mode 100644 docs/methods.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 013fbeb..aa77dd6 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -34,8 +34,12 @@ export default defineConfig({ link: '/getting-started' }, { - text: 'Additional Details', - link: '/additional-details' + text: 'Methods', + link: '/methods' + }, + { + text: 'Broadcasting Events', + link: '/broadcasting-events' } ] }, diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..23cd200 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,49 @@ +# Kitbag Events Documentation + +Welcome to the Kitbag Events documentation. This package provides a simple, lightweight event bus written in TypeScript with cross-tab broadcasting support. + +## Documentation Structure + +### Getting Started +- **Getting Started** (`/getting-started`) - Complete introduction with examples +- **Methods** (`/methods`) - Comprehensive API reference +- **Broadcasting Events** (`/broadcasting-events`) - Cross-tab communication guide + +### API Reference +- **Functions** - `createEmitter` function documentation +- **Types** - All TypeScript type definitions +- **Errors** - Error classes and handling + +## Quick Start + +```ts +import { createEmitter } from '@kitbag/events' + +type Events = { + userLogin: { userId: string } + messageReceived: { content: string } +} + +const emitter = createEmitter() + +emitter.on('userLogin', ({ userId }) => { + console.log(`User ${userId} logged in`) +}) + +emitter.emit('userLogin', { userId: '123' }) +``` + +## Key Features + +- **Type Safe**: Full TypeScript support with typed events +- **Cross-Tab Support**: Built-in broadcasting across browser tabs +- **Rich API**: on, off, once, next, emit, count, clear methods +- **Abort Support**: Automatic cleanup with AbortSignal +- **Promise Based**: Async event waiting with timeout support +- **Zero Dependencies**: Lightweight and fast + +## Need Help? + +- [GitHub Issues](https://github.com/kitbagjs/events/issues) +- [Discord Community](https://discord.gg/zw7dpcc5HV) +- [NPM Package](https://www.npmjs.com/package/@kitbag/events) diff --git a/docs/additional-details.md b/docs/additional-details.md deleted file mode 100644 index e5655bc..0000000 --- a/docs/additional-details.md +++ /dev/null @@ -1,116 +0,0 @@ -# Additional Details - -## Events Type - -The `Events` type defines what events can be emitted and their payload. - -```ts -type Events = { - ping: 'pong', - userCreated: User, - eventWithNoPayload: void, -} -``` - -## Broadcast Channel - -By default Kitbag Events only emits events within the document. Alternatively, you can configure Events to use a [Broadcast Channel](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel) which enables you to broadcast events across different windows, tabs, frames or iframes of the same origin. - -```ts -import { createEmitter } from '@kitbag/events' - -const emitter = createEmitter({ - broadcastChannel: 'MyChannelName' -}) -``` - -You can also set/change the broadcast channel after creating an emitter by using `emitter.setOptions({ broadcastChannel: 'MyChannelName '})` - -## Single Event Handler - -Define a handler for a single event called "hello" - -```ts -emitter.on('hello', value => { - console.log(value) // "world" -}) -``` - -## Global Event Handler - -Every event emitted will trigger this callback - -```ts -emitter.on(event => { - console.log(event) // { kind: 'hello', payload: 'world' } -}) -``` - -## Single Use Handler - -Listen for a single event and then automatically remove the handler - -```ts -emitter.once(...) -``` - -## Waiting for an event - -Wait for an event to be emitted and get the payload using the `next` method. - -```ts -const payload = await emitter.next() // Wait for any event -const payload = await emitter.next('hello') // Wait for the hello event -``` - -You can also pass an options object to the `next` method to set a timeout. - -```ts -const payload = await emitter.next({ timeout: 1000 }) // Wait for any event with a timeout of 1 second -const payload = await emitter.next('hello', { timeout: 1000 }) // Wait for the hello event with a timeout of 1 second -``` - -:::info -If the event is not emitted before the timeout, the `next` method will reject with an `EmitterTimeoutError`. -::: - -## Removing listeners - -### Return Value - -Emitter.on returns the off method for removing an event handler - -```ts -const off = emitter.on(...) -``` - -### Manually remove an event handler - -```ts -const handler = (value) => console.log(value) -const globalHandler = (event) => console.log(event) - -emitter.on('hello', handler) -emitter.on(globalHandler) - -emitter.off('hello', handler) -emitter.off(globalHandler) -``` - -### Remove all handlers for a given event - -```ts -const handler1 = (value) => console.log(value) -const handler2 = (value) => console.log(value) - -emitter.on('hello', handler1) -emitter.on('hello', handler2) - -emitter.off('hello') -``` - -### Remove all handlers for all events - -```ts -emitter.clear() -``` diff --git a/docs/broadcasting-events.md b/docs/broadcasting-events.md new file mode 100644 index 0000000..0e1769a --- /dev/null +++ b/docs/broadcasting-events.md @@ -0,0 +1,65 @@ +# Broadcasting Events + +Kitbag Events supports broadcasting events across multiple browser tabs/windows using the [`BroadcastChannel`](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API) API. + +## Setup + +Pass a channel name when creating your emitter: + +```ts +import { createEmitter } from '@kitbag/events' + +type Events = { + userLogin: { userId: string } + messageReceived: { content: string } +} + +const emitter = createEmitter({ + broadcastChannel: 'my-app-events' +}) +``` + +## How It Works + +When you specify a `broadcastChannel`: + +1. **Local emission**: Events are processed by handlers in the current tab +2. **Cross-tab broadcast**: Events are automatically sent to all other tabs using the same channel name +3. **Automatic reception**: Other tabs automatically receive and process broadcasted events + +## Example: Multi-tab Chat + +```ts +// Tab 1: Send message +emitter.emit('messageReceived', { content: 'Hello from Tab 1!' }) + +// Tab 2: Automatically receives the message +emitter.on('messageReceived', ({ content }) => { + console.log('Message received:', content) // "Hello from Tab 1!" +}) +``` + +## Channel Naming + +Use descriptive, unique channel names to avoid conflicts: + +```ts +// Good - specific to your app +broadcastChannel: 'my-chat-app-v1' + +// Good - includes user context +broadcastChannel: `user-${userId}-events` + +// Avoid - too generic +broadcastChannel: 'events' +``` + +## Use Cases + +- **Real-time updates**: Sync user actions across tabs +- **Session management**: Notify all tabs when user logs out +- **Data synchronization**: Keep multiple tabs in sync +- **Notifications**: Broadcast system-wide alerts + +## Structured Clone +Data sent is serialized using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). diff --git a/docs/getting-started.md b/docs/getting-started.md index cd60322..c3636d5 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -2,7 +2,7 @@ ## Installation -Install Kitbag Events with your favorite package manager +Install Kitbag Events with your favorite package manager: ```bash # bun @@ -13,29 +13,100 @@ yarn add @kitbag/events npm install @kitbag/events ``` -### Create an Emitter +## Basic Usage + +Create an emitter with typed events: ```ts import { createEmitter } from '@kitbag/events' type Events = { - hello: 'world' + userLogin: { userId: string; timestamp: number } + userLogout: { userId: string } + messageReceived: { from: string; content: string } } -export const emitter = createEmitter() +const emitter = createEmitter() +``` + +## Event Handling + +### Listen to specific events: + +```ts +// Listen to user login events +emitter.on('userLogin', ({ userId, timestamp }) => { + console.log(`User ${userId} logged in at ${timestamp}`) +}) + +// Listen to message events +emitter.on('messageReceived', ({ from, content }) => { + console.log(`Message from ${from}: ${content}`) +}) +``` + +### Global event handler (runs on all events): + +```ts +emitter.on(({ kind, payload }) => { + console.log(`Event ${kind} occurred:`, payload) +}) ``` -### Add Listeners +### One-time event listeners: ```ts -emitter.on('hello', value => { - console.log(value) +emitter.once('userLogin', ({ userId }) => { + console.log(`First login for user ${userId}`) }) ``` -### Emit Events +## Emitting Events + +```ts +// Emit user login +emitter.emit('userLogin', { userId: '123', timestamp: Date.now() }) + +// Emit message +emitter.emit('messageReceived', { from: 'alice', content: 'Hello world!' }) +``` + +## Waiting for Events + +```ts +// Wait for next user login +const loginData = await emitter.next('userLogin') +console.log('Next login:', loginData) + +// Wait for any event with timeout +const nextEvent = await emitter.next({ timeout: 5000 }) +console.log('Next event:', nextEvent) +``` + +## Cleanup ```ts -emitter.emit('hello', 'world') -// console logs "world" +// Remove specific handler +const handler = (data: any) => console.log(data) +emitter.on('userLogin', handler) +emitter.off('userLogin', handler) + +// Remove all handlers for an event +emitter.off('userLogin') + +// Clear all handlers +emitter.clear() +``` + +## Abort Signal Support + +```ts +const controller = new AbortController() + +emitter.on('userLogin', ({ userId }) => { + console.log(`User ${userId} logged in`) +}, { signal: controller.signal }) + +// Later, abort the listener +controller.abort() ``` diff --git a/docs/index.md b/docs/index.md index b87301a..4f243c9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,23 +4,26 @@ layout: home hero: name: "Kitbag Events" image: "/kitbag-logo.svg" - tagline: "A simple lightweight event bus written in Typescript." + tagline: "A simple lightweight event bus written in TypeScript with cross-tab broadcasting support." actions: - theme: brand text: Get Started link: /getting-started - theme: alt - text: Additional Details - link: /additional-details + text: View API + link: /api features: - title: Simple - details: Tiny, zero dependencies - - title: Type safety - details: Type safe events and payload - - title: Useful API - details: Includes on, off, once, next, emit, count, and clear - - title: Support Global Handlers - details: Setup handlers that run on all events ---- + details: Tiny, zero dependencies, easy to use + - title: Type Safe + details: Full TypeScript support with typed events and payloads + - title: Cross-Tab Support + details: Built-in broadcasting across browser tabs and windows + - title: Rich API + details: on, off, once, next, emit, count, clear, and global handlers + - title: Abort Support + details: Automatic cleanup with AbortSignal integration + - title: Promise Based + details: Async event waiting with timeout support diff --git a/docs/methods.md b/docs/methods.md new file mode 100644 index 0000000..eb65fc4 --- /dev/null +++ b/docs/methods.md @@ -0,0 +1,205 @@ +# Methods + +## On + +Register event handlers for specific events or globally. + +### Event-specific handler + +**Parameters:** +- `event`: Event name to listen to +- `handler`: Function called when event occurs +- `options.signal`: AbortSignal to automatically remove handler + +**Returns:** Unsubscribe function + +**Example:** +```ts +const unsubscribe = emitter.on('userLogin', ({ userId }) => { + console.log(`User ${userId} logged in`) +}) + +// Later remove the handler +unsubscribe() +``` + +### Global handler + +**Parameters:** +- `globalEventHandler`: Function called for all events +- `options.signal`: AbortSignal to automatically remove handler + +**Returns:** Unsubscribe function + +**Example:** +```ts +const unsubscribe = emitter.on(({ kind, payload }) => { + console.log(`Event ${kind}:`, payload) +}) +``` + +## Once + +Register a one-time event handler that automatically removes itself after execution. + +### Event-specific handler + +**Parameters:** +- `event`: Event name to listen to +- `handler`: Function called once when event occurs +- `options.signal`: AbortSignal to automatically remove handler + +**Returns:** Unsubscribe function + +**Example:** +```ts +emitter.once('userLogin', ({ userId }) => { + console.log(`First login for user ${userId}`) +}) +``` + +### Global handler + +**Parameters:** +- `globalEventHandler`: Function called once for the next event +- `options.signal`: AbortSignal to automatically remove handler + +**Returns:** Unsubscribe function + +## Next + +Wait for the next occurrence of an event or any event. + +### Wait for specific event + +**Parameters:** +- `event`: Event name to wait for +- `options.timeout`: Milliseconds to wait before rejecting + +**Returns:** Promise that resolves with event payload + +**Example:** +```ts +try { + const loginData = await emitter.next('userLogin', { timeout: 5000 }) + console.log('Login occurred:', loginData) +} catch (error) { + if (error instanceof EmitterTimeoutError) { + console.log('No login within 5 seconds') + } +} +``` + +### Wait for any event + +**Parameters:** +- `options.timeout`: Milliseconds to wait before rejecting + +**Returns:** Promise that resolves with event info + +**Example:** +```ts +const nextEvent = await emitter.next({ timeout: 10000 }) +console.log(`Event ${nextEvent.kind} occurred:`, nextEvent.payload) +``` + +## Emit + +Trigger an event with optional payload. + +**Parameters:** +- `event`: Event name to emit +- `payload`: Data to pass to event handlers + +**Example:** +```ts +emitter.emit('userLogin', { userId: '123', timestamp: Date.now() }) +emitter.emit('userLogout', { userId: '123' }) +``` + +## Off + +Remove event handlers. + +### Remove specific handler + +**Parameters:** +- `event`: Event name +- `handler`: Specific handler to remove + +**Example:** +```ts +const handler = (data: any) => console.log(data) +emitter.on('userLogin', handler) +emitter.off('userLogin', handler) +``` + +### Remove all handlers for event + +**Parameters:** +- `event`: Event name to clear all handlers for + +**Example:** +```ts +emitter.off('userLogin') // Removes all userLogin handlers +``` + +### Remove global handler + +**Parameters:** +- `globalEventHandler`: Global handler to remove + +**Example:** +```ts +const globalHandler = ({ kind, payload }) => console.log(kind, payload) +emitter.on(globalHandler) +emitter.off(globalHandler) +``` + +## Count + +Get the number of registered handlers. + +### Count handlers for specific event + +**Parameters:** +- `event`: Event name to count handlers for +- `options.global`: Include global handlers in count + +**Returns:** Number of handlers + +**Example:** +```ts +const eventCount = emitter.count('userLogin') +const totalCount = emitter.count('userLogin', { global: true }) +``` + +### Count global handlers + +**Returns:** Number of global handlers + +**Example:** +```ts +const globalCount = emitter.count() +``` + +## Clear + +Remove all event handlers and global handlers. + +**Example:** +```ts +emitter.clear() // Removes all handlers +``` + +## Set Options + +Update emitter configuration after creation. + +**Parameters:** +- `options.broadcastChannel`: Channel name for cross-tab communication + +**Example:** +```ts +emitter.setOptions({ broadcastChannel: 'new-channel' }) +``` From 7d4016b33af959681b44b528835c3df8067a9d03 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Mon, 18 Aug 2025 22:31:21 -0500 Subject: [PATCH 4/6] Tweak the index --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 4f243c9..9b99d76 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,6 +24,6 @@ features: details: on, off, once, next, emit, count, clear, and global handlers - title: Abort Support details: Automatic cleanup with AbortSignal integration - - title: Promise Based + - title: Promise Support details: Async event waiting with timeout support From d6b6587d72f1874c0e0c09a6f0b46b7d11fa10a1 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Mon, 18 Aug 2025 22:31:29 -0500 Subject: [PATCH 5/6] Generate type docs --- docs/api/functions/createEmitter.md | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/api/functions/createEmitter.md b/docs/api/functions/createEmitter.md index 9b4a0d6..a770daa 100644 --- a/docs/api/functions/createEmitter.md +++ b/docs/api/functions/createEmitter.md @@ -30,6 +30,40 @@ clear: () => void; `void` +### count() + +```ts +count: () => number(event, options?) => number; +``` + +#### Type Parameters + +| Type Parameter | +| ------ | +| `E` *extends* `string` \| `number` \| `symbol` | + +#### Returns + +`number` + +#### Type Parameters + +| Type Parameter | +| ------ | +| `E` *extends* `string` \| `number` \| `symbol` | + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `event` | `E` | +| `options`? | \{ `global`: `boolean`; \} | +| `options.global`? | `boolean` | + +#### Returns + +`number` + ### emit() ```ts From aa548777bbeb6a3d1ba1980ce7ed2681caf2e874 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Tue, 19 Aug 2025 22:45:13 -0500 Subject: [PATCH 6/6] Delete the readme --- docs/README.md | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 23cd200..0000000 --- a/docs/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Kitbag Events Documentation - -Welcome to the Kitbag Events documentation. This package provides a simple, lightweight event bus written in TypeScript with cross-tab broadcasting support. - -## Documentation Structure - -### Getting Started -- **Getting Started** (`/getting-started`) - Complete introduction with examples -- **Methods** (`/methods`) - Comprehensive API reference -- **Broadcasting Events** (`/broadcasting-events`) - Cross-tab communication guide - -### API Reference -- **Functions** - `createEmitter` function documentation -- **Types** - All TypeScript type definitions -- **Errors** - Error classes and handling - -## Quick Start - -```ts -import { createEmitter } from '@kitbag/events' - -type Events = { - userLogin: { userId: string } - messageReceived: { content: string } -} - -const emitter = createEmitter() - -emitter.on('userLogin', ({ userId }) => { - console.log(`User ${userId} logged in`) -}) - -emitter.emit('userLogin', { userId: '123' }) -``` - -## Key Features - -- **Type Safe**: Full TypeScript support with typed events -- **Cross-Tab Support**: Built-in broadcasting across browser tabs -- **Rich API**: on, off, once, next, emit, count, clear methods -- **Abort Support**: Automatic cleanup with AbortSignal -- **Promise Based**: Async event waiting with timeout support -- **Zero Dependencies**: Lightweight and fast - -## Need Help? - -- [GitHub Issues](https://github.com/kitbagjs/events/issues) -- [Discord Community](https://discord.gg/zw7dpcc5HV) -- [NPM Package](https://www.npmjs.com/package/@kitbag/events)