@vanillaspa/event-bus 1.5.0 → 2.0.1
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.
- package/index.js +97 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,11 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Memory-safe, context-scoped custom event bus.
|
|
3
|
+
*
|
|
4
|
+
* Listeners are keyed by a `context` value (typically a host `HTMLElement`).
|
|
5
|
+
* When a context is garbage-collected its listeners are silently dropped —
|
|
6
|
+
* no manual cleanup required for GC'd elements.
|
|
7
|
+
*
|
|
8
|
+
* **Auto-cleanup:** The first time a listener is registered for an
|
|
9
|
+
* `HTMLElement` context, the bus attaches a native `component:disconnected`
|
|
10
|
+
* listener directly on that element. When the web-components package fires
|
|
11
|
+
* that event in `disconnectedCallback`, all event bus listeners for the
|
|
12
|
+
* context are removed automatically. No manual cleanup needed.
|
|
13
|
+
*
|
|
14
|
+
* Exposed on `window.eventbus` (frozen) by `main.js` after import.
|
|
15
|
+
*
|
|
16
|
+
* @module event-bus
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** @type {'eventbus'} */
|
|
1
20
|
export const name = 'eventbus';
|
|
2
21
|
|
|
3
|
-
|
|
4
|
-
const
|
|
22
|
+
/** @type {WeakMap<object, Map<string, Function[]>>} context → (type → listeners[]) */
|
|
23
|
+
const contextListeners = new WeakMap();
|
|
24
|
+
|
|
25
|
+
/** @type {WeakSet<object>} Tracks contexts that already have auto-cleanup registered. */
|
|
26
|
+
const autoCleanup = new WeakSet();
|
|
5
27
|
|
|
28
|
+
/** @type {Map<string, Set<WeakRef<object>>>} type → Set of weak refs to registered contexts */
|
|
29
|
+
const typeIndex = new Map();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a listener for `type` events scoped to `context`.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} type - Event type string (e.g. `'sqlite:statement'`).
|
|
35
|
+
* @param {function(Event): void} listener - Handler to invoke on dispatch.
|
|
36
|
+
* @param {object} context - Scoping key, typically the component's host `HTMLElement`.
|
|
37
|
+
*/
|
|
6
38
|
export function addEventListener(type, listener, context) {
|
|
7
|
-
if (!contextListeners.has(context)) {
|
|
39
|
+
if (!contextListeners.has(context)) {
|
|
8
40
|
contextListeners.set(context, new Map());
|
|
41
|
+
if (context instanceof HTMLElement && !autoCleanup.has(context)) {
|
|
42
|
+
autoCleanup.add(context);
|
|
43
|
+
context.addEventListener('component:disconnected', () => removeAllEventListeners(context), { once: true });
|
|
44
|
+
}
|
|
9
45
|
}
|
|
10
46
|
const byType = contextListeners.get(context);
|
|
11
47
|
if (!byType.has(type)) byType.set(type, []);
|
|
@@ -17,6 +53,13 @@ export function addEventListener(type, listener, context) {
|
|
|
17
53
|
if (!alreadyRegistered) refs.add(new WeakRef(context));
|
|
18
54
|
}
|
|
19
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Remove a previously registered listener.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} type - Event type string.
|
|
60
|
+
* @param {function(Event): void} listener - The exact listener reference to remove.
|
|
61
|
+
* @param {object} context - The context the listener was registered under.
|
|
62
|
+
*/
|
|
20
63
|
export function removeEventListener(type, listener, context) {
|
|
21
64
|
const byType = contextListeners.get(context);
|
|
22
65
|
if (!byType?.has(type)) return;
|
|
@@ -39,6 +82,11 @@ export function removeEventListener(type, listener, context) {
|
|
|
39
82
|
}
|
|
40
83
|
}
|
|
41
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Remove all listeners registered under `context`, across all event types.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} context - The context whose listeners should all be removed.
|
|
89
|
+
*/
|
|
42
90
|
export function removeAllEventListeners(context) {
|
|
43
91
|
contextListeners.delete(context);
|
|
44
92
|
for (const refs of typeIndex.values()) {
|
|
@@ -51,9 +99,19 @@ export function removeAllEventListeners(context) {
|
|
|
51
99
|
}
|
|
52
100
|
}
|
|
53
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Dispatch `event` to all matching listeners.
|
|
104
|
+
*
|
|
105
|
+
* If `context` is provided, only listeners registered under that exact context
|
|
106
|
+
* receive the event. If omitted, falls back to `event.detail?.target` or
|
|
107
|
+
* `event.target` as an implicit context. If that is also absent the event is
|
|
108
|
+
* delivered to all registered listeners for its type (true broadcast).
|
|
109
|
+
*
|
|
110
|
+
* @param {Event|CustomEvent} event - The event to dispatch.
|
|
111
|
+
* @param {object} [context] - Optional explicit routing context.
|
|
112
|
+
*/
|
|
54
113
|
export function dispatchEvent(event, context = undefined) {
|
|
55
114
|
if (!context) context = event instanceof CustomEvent ? event.detail?.target : event.target;
|
|
56
|
-
|
|
57
115
|
const refs = typeIndex.get(event.type);
|
|
58
116
|
if (refs) {
|
|
59
117
|
for (const ref of refs) {
|
|
@@ -64,3 +122,38 @@ export function dispatchEvent(event, context = undefined) {
|
|
|
64
122
|
}
|
|
65
123
|
}
|
|
66
124
|
}
|
|
125
|
+
|
|
126
|
+
// Contract & Bridge
|
|
127
|
+
export function mountBridge(contract, host) {
|
|
128
|
+
const { namespace, events, handlers, responseDetail = {} } = contract;
|
|
129
|
+
|
|
130
|
+
for (const [action, { past }] of Object.entries(events)) {
|
|
131
|
+
addEventListener(`${namespace}:${action}`, async (event) => {
|
|
132
|
+
try {
|
|
133
|
+
const result = await handlers[action](event.detail);
|
|
134
|
+
const detail = responseDetail[action]?.(result, event.detail) ?? { result };
|
|
135
|
+
dispatchEvent(new CustomEvent(`${namespace}:${past}:${event.timeStamp}`, { detail }), host);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
dispatchEvent(new CustomEvent(`${namespace}:error:${event.timeStamp}`, {
|
|
138
|
+
detail: { error: error.message, action }
|
|
139
|
+
}), host);
|
|
140
|
+
}
|
|
141
|
+
}, host);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function useContract(contract, host) {
|
|
146
|
+
const { namespace, events } = contract;
|
|
147
|
+
|
|
148
|
+
function call(action) {
|
|
149
|
+
return (detail) => new Promise((resolve, reject) => {
|
|
150
|
+
const req = new CustomEvent(`${namespace}:${action}`, { detail });
|
|
151
|
+
const { past } = events[action];
|
|
152
|
+
addEventListener(`${namespace}:${past}:${req.timeStamp}`, (e) => resolve(e.detail), host);
|
|
153
|
+
addEventListener(`${namespace}:error:${req.timeStamp}`, (e) => reject(new Error(e.detail.error)), host);
|
|
154
|
+
dispatchEvent(req, host);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return Object.fromEntries(Object.keys(events).map(action => [action, call(action)]));
|
|
159
|
+
}
|
package/package.json
CHANGED