Skip to content

Commit 9dbc342

Browse files
committed
First Public Release
0 parents  commit 9dbc342

16 files changed

+4552
-0
lines changed

.editorconfig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# See editorconfig.org
2+
root = true
3+
4+
[*]
5+
charset = utf-8
6+
end_of_line = lf
7+
indent_size = 4
8+
indent_style = space
9+
insert_final_newline = true
10+
trim_trailing_whitespace = true
11+
12+
[*.md]
13+
insert_final_newline = false
14+
trim_trailing_whitespace = false

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Ensure consistent line endings
2+
* text=auto
3+
4+
# Ignore Handlebars for GitHub language stats
5+
*.hbs linguist-detectable=false

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.DS_Store
2+
node_modules

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
This project adheres to [Semantic Versioning](https://semver.org) and follows the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
6+
7+
---
8+
9+
## [1.5.0] – 2025-04-06
10+
11+
### Added
12+
13+
- This marks the first public release of **Publisher**
14+
- Publish/Subscribe architecture with topic hierarchy
15+
- Wildcard support in subscriptions (`*`)
16+
- Persistent message storage and late delivery
17+
- Subscription priority and invocation limits
18+
- Conditional subscriptions via `condition` function
19+
- Global configuration via `configure()`
20+
- Support for both ES Modules and CommonJS (`require`) as well as UMD (global `publisher`)
21+
- Minified build output via Rollup (ESM + UMD)
22+
23+
---

CONTRIBUTING.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Contributing
2+
3+
You're very welcome to submit issues or suggest improvements! However, for the moment, code changes will only be accepted by the project maintainer.
4+
5+
## Project Maintainer
6+
7+
- [Frank Kudermann](https://github.com/alphanull) ([@alphanull](https://github.com/alphanull)) – Author & Maintainer
8+
9+
## How to Contribute?
10+
11+
Feel free to:
12+
13+
- Open issues with questions, suggestions, or bug reports.
14+
- Provide feedback and feature requests.
15+
16+
Thank you for your understanding! 😊

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright © 2015-present Frank Kudermann @ alphanull.de
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
![License](https://img.shields.io/github/license/alphanull/publisher)
2+
![Version](https://img.shields.io/badge/version-1.5-blue)
3+
[![JSDoc](https://img.shields.io/badge/docs-JSDoc-blue)](./docs/publisher.md)
4+
![Size](https://img.shields.io/badge/gzipped~2kb-brightgreen)
5+
6+
# @alphanull/publisher
7+
8+
Publisher is a JavaScript Publish/Subscribe (Pub/Sub) library crafted to handle event-driven communication. Provides pub/sub functionality with extensive wildcard support, async/sync publishing, priority and invocation options, content based filtering & more.
9+
10+
The heart of Publisher lies in its uniquely optimized hierarchical data structure, providing fast subscriber matching even with extensive subscription sets. Unlike traditional flat Pub/Sub systems, Publisher allows you to easily organize events into structured topics and leverage wildcard subscriptions for additional flexibility.
11+
12+
Whether you're building scalable web applications or complex front-end architectures, Publisher ensures your events and notifications are handled gracefully and reliably, delivering ease of use combined with powerful features.
13+
14+
## Features
15+
16+
- **Topic Hierarchy & Wildcards**: Manage event complexity with a structured hierarchy and wildcard topic matching.
17+
- **Persisted Messages**: Ensure subscribers never miss critical events, delivering persistent messages immediately upon subscribing.
18+
- **Priority & Invocations**: Gain fine-grained control over execution order and limit subscription triggers, improving predictability and efficiency.
19+
- **Async & Exception Handling**: Dispatch events asynchronously or synchronously with built-in exception handling.
20+
- **Conditional Execution**: Execute subscriptions only when specific conditions are met.
21+
- **Global Configuration**: Configure default behavior globally for asynchronous dispatch, error handling, and unsubscribing behavior.
22+
23+
---
24+
25+
## Installation
26+
27+
28+
### via NPM
29+
30+
**ATTN: Package is not on npm yet due to namespace clearance!**
31+
32+
```bash
33+
npm install @alphanull/publisher <<< not here yet!
34+
```
35+
36+
### via CDN
37+
38+
**Also, no CDN (yet)**
39+
40+
[Download latest version](https://??????) from ????????
41+
42+
### via GitHub
43+
44+
[Download release](https://github.com/alphanull/publisher/releases) from GitHub
45+
46+
------
47+
48+
## Usage
49+
50+
### 1. Initialization
51+
52+
publisher can be used as ES6 module (recommended) but also via `require` in NodeJS or with direct access to a global variable:
53+
54+
## ES6
55+
56+
```javascript
57+
import { publish, subscribe, unsubscribe } from '@alphanull/publisher';
58+
```
59+
60+
## CommonJS
61+
62+
```javascript
63+
const { publish, subscribe, unsubscribe } = require('@alphanull/publisher');
64+
```
65+
66+
## Global Variable
67+
68+
```html
69+
<script src="path/to/publisher.min.cjs"></script>
70+
```
71+
72+
```javascript
73+
const { publish, subscribe, unsubscribe } = publisher;
74+
```
75+
76+
77+
### 2. Basic Usage
78+
79+
Quickly set up a simple Pub/Sub interaction:
80+
81+
```javascript
82+
import { publish, subscribe, unsubscribe } from '@alphanull/publisher';
83+
84+
const handler = data => {
85+
console.log(`User logged in: ${data.username}`);
86+
}
87+
88+
// Receiver: subscribe to a specific topic
89+
const token = subscribe('login', handler);
90+
91+
// Sender: publish an event
92+
publish('login', { username: 'Alice' });
93+
94+
// Receiver: unsubscribe using the token (recommended)
95+
unsubscribe(token);
96+
97+
// Receiver: alternatively, unsubscribe using topic and handler
98+
unsubscribe('login', handler);
99+
```
100+
101+
---
102+
103+
### 3. Hierarchy and Wildcards
104+
105+
By utilizing topic hierarchies and wildcards, you can subscribe to multiple events. A hierachy is created by using the `/` delimiter to create topic segments, while a `*` is used to match any topic segment:
106+
107+
```javascript
108+
// Subscribe to ALL topics
109+
subscribe('*', (data, topic) => {
110+
console.log(`Event ${topic} received:`, data);
111+
});
112+
113+
// Subscribe to all "user"-related topics, INCLUDING "user"
114+
subscribe('user', (data, topic) => {
115+
console.log(`Event ${topic} received:`, data);
116+
});
117+
118+
// Subscribe to all "user"-related topics, EXCLUDING "user"
119+
subscribe('user/*', (data, topic) => {
120+
console.log(`Event ${topic} received:`, data);
121+
});
122+
123+
// Matching multiple topics with wildcards
124+
subscribe('app/*/update', (data, topic) => {
125+
console.log(`Update from ${topic}:`, data);
126+
});
127+
128+
publish('user/logout', { username: 'Bob' }); // triggers first, second & third subscriber
129+
publish('app/profile/update', { username: 'Charlie' }); // triggers first and fourth subscribers
130+
publish('app/settings/update', { theme: 'dark' }); // triggers first and fourth subscribers
131+
```
132+
133+
---
134+
135+
### 4. Advanced Unsubscribe: Multiple Tokens, Lenient Unsubscribe
136+
137+
You can also use an array of tokens to quickly unsubscribe multiple handlers. In addition, adding `true` to the second argument (or the third, in case you use topic/handler for unsubscribe) does not fail with an error if the matching token was not found.
138+
139+
```javascript
140+
const tokens = [
141+
subscribe('topic/1', handler),
142+
subscribe('topic/2', handler)
143+
];
144+
145+
// Batch unsubscribe
146+
unsubscribe(tokens);
147+
148+
// Lenient unsubscribe, silently ignores non-existing tokens
149+
unsubscribe(9999, true);
150+
```
151+
152+
---
153+
154+
### 5. Async and Sync Usage, Cancellation
155+
156+
By default, all events are sent asynchronously. You can override this behavior globally (see 10.) or with individual `publish` actions by using `async: false` as an option. In addition, when using synchronous `publish`, any subscriber is able to cancel an event, so that subsequent subscribers are not notified anymore. So basically, this works similar to the cancellation of DOM Events.
157+
158+
```javascript
159+
// return false in a handler cancels the chain
160+
subscribe('sync/event', () => false);
161+
162+
// Synchronous event publishing can be canceled
163+
publish('sync/event', {}, { async: false, cancelable: true });
164+
```
165+
166+
---
167+
168+
### 6. Priority
169+
170+
Usually, subscribers are notifed in the order they subscribed, i.e. the first subscriber is receiving the first message. You can change this behavior adding a `priority` option, where higher numbers are executed first, with `0` being the default.
171+
172+
```javascript
173+
// Control subscriber order with priorities
174+
subscribe('priority/event', () => console.log('second'), { priority: 1 });
175+
subscribe('priority/event', () => console.log('first'), { priority: 2 });
176+
177+
publish('priority/event');
178+
```
179+
180+
---
181+
182+
### 7. Invocations
183+
184+
It is also possible to limit the number of handler invocations by adding the `invocations` option, this being a positive number counting down when the handler is called. Once the counter reaches `0` the handler is automatically unsubscribed. For example, the following code executes the handler only on the first `publish` occurence:
185+
186+
```javascript
187+
// Limit subscription invocations
188+
subscribe('limited/event', () => console.log('I only execute once'), { invocations: 1 });
189+
190+
publish('limited/event'); // triggers handler
191+
publish('limited/event'); // not triggered anymore, handler was unsubscribed
192+
```
193+
194+
### 8. Conditional Execution
195+
196+
Run subscriptions based on conditional logic, so that the handler is only invoked if the function specified by the `condition` option returns true:
197+
198+
```javascript
199+
subscribe('data/event', data => {
200+
console.log('Condition met:', data);
201+
}, {
202+
condition: data => data.status === 'success'
203+
});
204+
205+
publish('data/event', { status: 'success' }); // triggers subscriber
206+
publish('data/event', { status: 'error' }); // ignored
207+
```
208+
209+
---
210+
211+
### 9. Persistency
212+
213+
Ensure certain messages are received even when the subscription is done after the actual message was already sent. For this to happen, _both_ `publish` and `subscribe` have to use the `persist: true` option. It is also possible to remove a perssitent message later on using `removePersistentMessage`.
214+
215+
```javascript
216+
import { publish, subscribe, removePersistentMessage } from './publisher.js';
217+
218+
// make message persistent
219+
publish('app/ready', { status: 'ready' }, { persist: true });
220+
221+
// Subscribers immediately receive persistent messages upon subscription
222+
subscribe('app/ready', data => console.log('Persistently received:', data), { persist: true });
223+
224+
// after removing, later subscriber don't receive the event anymore
225+
removePersistentMessage('app/ready');
226+
```
227+
228+
---
229+
230+
### 10. Error Handling
231+
232+
By default, if a handler throws an Error, it is caught by the publisher so that subsequent subscribers are still being executed. Instead the error is output to the console (if possible). This behavior can be changed globally, or per `publish` so that exceptions are not caught anymore.
233+
234+
```javascript
235+
subscribe('error/event', () => {
236+
throw new Error('Subscriber error!');
237+
});
238+
239+
subscribe('error/event', () => {
240+
console.log('I still might be executed');
241+
});
242+
243+
// Errors caught internally, other subscribers remain unaffected
244+
publish('error/event', data , { handleExceptions: true });
245+
246+
// Throws an error, publishing is halted
247+
publish('error/event', data , { handleExceptions: false });
248+
```
249+
250+
---
251+
252+
### 11. Global Configuration
253+
254+
Configure Publisher.js globally to tailor its behavior. All subsequent actions will use the newly set option(s), unless locally overidden.
255+
256+
```javascript
257+
import { configure } from './publisher.js';
258+
259+
// Equivalent to the default configuration
260+
configure({
261+
async: true, // Global async dispatch
262+
handleExceptions: true, // Global error handling
263+
lenientUnsubscribe: true // No errors on unsubscribing non-existent subscribers
264+
});
265+
```
266+
267+
---
268+
269+
## Docs
270+
271+
For more detailled docs, see [JSDoc Documentation](docs/publisher.md)
272+
273+
## License
274+
275+
[MIT](https://opensource.org/license/MIT)
276+
277+
Copyright © 2015-present Frank Kudermann @ alphanull.de

dist/publisher.min.cjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/*!
2+
* Publisher – Javascript Pub/Sub library
3+
* @license MIT
4+
* © 2013–2025 Frank Kudermann @ alphanull
5+
*/
6+
!function(e,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i((e="undefined"!=typeof globalThis?globalThis:e||self).publisher={})}(this,(function(e){"use strict";const i={async:!0,handleExceptions:!1,lenientUnsubscribe:!1},n=new Map,o=new Map,t=new Map;let s=-1;function r(e,t,s){const r=t===Boolean(t)?t:s,c=void 0===r?i.lenientUnsubscribe:r,a=e=>{const i=n.get(e);if(void 0===i){if(!0===c)return;throw new Error(`Unsubscribe failed. Did not find subscriber for token: ${e}`)}l(i.topic.split("/"),i,o),n.delete(e)};if(void 0===e){if(!0===c)return;throw new Error("Unsubscribe failed. No Arguments specified.")}if(Array.isArray(e))e.forEach((e=>a(e)));else if(!isNaN(parseFloat(e))&&isFinite(e))a(e);else{if(void 0===t){if(!0===c)return;throw new Error(`Unsubscribe failed. No handler for topic based unsubscribe specified ${e}`)}for(const[,i]of n)if(i.handler===t&&i.topic===e){l(e.split("/"),i,o),n.delete(i.token);break}}}function c(e,o,t,s={}){if(!n.has(e.token))return;e.options.invocations>0&&(e.options.invocations-=1,e.options.invocations<=0&&r(e.token));const{handler:c}=e;if(!0!==s.handleExceptions&&!0!==i.handleExceptions)return c(t,o);try{return c(t,o)}catch(e){window.console&&window.console.error&&window.console.error("Exception while executing publish handler: ",e)}}function a(e,n,o={},t,s,r=[]){const c=t.get("subscribers")||new Map,d=t.get("topics");for(const[,e]of c){const{condition:t}=e.options;(void 0===t||"[object Function]"===Object.prototype.toString.call(t)&&!0===t(n,s))&&(e.position=r.push(e),e.priority=e.options.priority||0,e.async=Boolean(o.async||e.options.async||i.async))}if(e.length&&d){const i=d.get(e[0]),t=d.get("*");void 0===t&&void 0===i||(void 0!==t&&a(e.slice(1,e.length),n,o,t,s,r),void 0!==i&&a(e.slice(1,e.length),n,o,i,s,r),e.shift())}return r}function d(e,i,n){const[o]=e;void 0===n.get("topics")&&n.set("topics",new Map);const t=n.get("topics");let s=t.get(o);void 0===s&&(s=new Map,t.set(o,s)),e.length<2?(void 0===s.get("subscribers")&&s.set("subscribers",new Map),s.get("subscribers").set(i.token,i)):(e.shift(),d(e,i,s))}function l(e,i,n){const[o]=e,t=n.get("topics"),s=t.get(o),r=s.get("subscribers");e.length<2?(r.delete(i.token),0===r.size&&s.delete("subscribers")):(e.shift(),l(e,i,s)),s.has("topics")&&0===s.get("topics").size&&s.delete("topics"),0===t.get(o).size&&t.delete(o)}e.configure=function(e){if(!e)throw new Error("Publisher configure: no options specified");void 0!==e.async&&(i.async=e.async),void 0!==e.handleExceptions&&(i.handleExceptions=e.handleExceptions),void 0!==e.lenientUnsubscribe&&(i.lenientUnsubscribe=e.lenientUnsubscribe)},e.publish=function(e,n,s={}){if(!0===s.persist&&t.set(e,{data:n,options:s}),e.indexOf("*")>-1)throw new Error("Publish topic cannot contain any wildcards.");const r=a(e.split("/"),n,s,o,e);let d;r.sort(((e,i)=>e.priority===i.priority?e.position-i.position:e.priority>i.priority?-1:1));const l=void 0===s.async?i.async:s.async;for(;d=r.shift();)if(l)setTimeout(c.bind(null,d,e,n,s),0);else if(!1===c(d,e,n,s)&&!1!==s.cancelable)return!1},e.removePersistentMessage=function(e){t.delete(e)},e.subscribe=function(e,r,a={}){const l=s+=1;if(void 0===e)throw new Error('Subscribe failed - "undefined" Topic.');if(e.includes("undefined"))throw new Error(`Subscribe for '${e}' failed - found 'undefined' in topic, this is almost always an error: ${l}`);if(void 0===r)throw new Error(`Subscribe for '${e}' failed - "undefined" Handler`);const f={token:l,topic:e,handler:r,options:a};if(n.set(l,f),d(e.split("/"),f,o),!0!==a.persist)return l;const p=new RegExp(`^${e.replace("*","(.+)")}(/.+)?$`);for(const[e,n]of t){if(e.match(p)){if(void 0===n.options.async?i.async:n.options.async)setTimeout(c.bind(null,f,e,n.data,a),0);else if(!1===c(f,e,n.data,a)&&!0===a.cancelable)break}}return l},e.unsubscribe=r}));

0 commit comments

Comments
 (0)