@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.
Files changed (2) hide show
  1. package/index.js +97 -4
  2. 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
- const contextListeners = new WeakMap(); // context -> Map<type, listener[]>
4
- const typeIndex = new Map(); // type -> Set<WeakRef<context>>
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)) { // first seen
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
@@ -27,5 +27,5 @@
27
27
  "url": "git+https://github.com/vanillaspa/event-bus.git"
28
28
  },
29
29
  "type": "module",
30
- "version": "1.5.0"
30
+ "version": "2.0.1"
31
31
  }