@therms/react-event-bus 1.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/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @therms/react-event-bus
2
+
3
+ An event-bus for use in React applications. This library provides a simple, type-safe way to handle cross-component communication using an event bus pattern.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @therms/react-event-bus
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### 1. Create an Event Notifier
14
+
15
+ Create an instance of the event notifier. You can define the payload type for type safety.
16
+
17
+ ```typescript
18
+ // events.ts
19
+ import { makeEventNotifier } from '@therms/react-event-bus';
20
+
21
+ // Define the payload type
22
+ type UserLoggedInPayload = {
23
+ userId: string;
24
+ timestamp: number;
25
+ };
26
+
27
+ export const userLoginEvent = makeEventNotifier<UserLoggedInPayload>('USER_LOGIN');
28
+ ```
29
+
30
+ ### 2. Publish Events
31
+
32
+ Trigger an event from anywhere in your application.
33
+
34
+ ```typescript
35
+ import { userLoginEvent } from './events';
36
+
37
+ userLoginEvent.notify({
38
+ userId: '12345',
39
+ timestamp: Date.now(),
40
+ });
41
+ ```
42
+
43
+ ### 3. Subscribe to Events (React Hook)
44
+
45
+ Use the `useEventListener` hook within your React components to listen for events. The hook automatically handles cleanup when the component unmounts.
46
+
47
+ ```tsx
48
+ import React from 'react';
49
+ import { userLoginEvent } from './events';
50
+
51
+ const UserStatus = () => {
52
+
53
+ userLoginEvent.useEventListener((payload) => {
54
+ console.log('User logged in:', payload.userId);
55
+ }, []); // Dependency array, similar to useEffect
56
+
57
+ return <div>Waiting for login...</div>;
58
+ };
59
+ ```
60
+
61
+ ### 4. Subscribe to Events (Vanilla JS/TS)
62
+
63
+ You can also subscribe outside of React components using the `listen` method.
64
+
65
+ ```typescript
66
+ import { userLoginEvent } from './events';
67
+
68
+ const unsubscribe = userLoginEvent.listen((payload) => {
69
+ console.log('Received event:', payload);
70
+ });
71
+
72
+ // Later, to stop listening:
73
+ unsubscribe();
74
+ ```
75
+
76
+ ## API
77
+
78
+ ### `makeEventNotifier<Payload>(name: string)`
79
+
80
+ Creates a new event notifier instance.
81
+
82
+ * `name`: A unique name for the event (mainly for debugging purposes).
83
+ * `Payload`: The type of data that will be passed with the event.
84
+
85
+ Returns an object with the following methods:
86
+
87
+ * `notify(payload: Payload)`: Triggers the event with the given payload.
88
+ * `useEventListener(listener: (payload: Payload) => void, deps: any[])`: A React hook to subscribe to the event.
89
+ * `listen(listener: (payload: Payload) => void)`: Subscribes to the event. Returns an unsubscribe function.
90
+ * `name`: The name of the event.
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,17 @@
1
+ export declare function makeEventNotifier<Payload>(name: string): {
2
+ /**
3
+ * Subscribe to events with a strong reference.
4
+ * The listener will remain active until manually unsubscribed.
5
+ */
6
+ listen: (listener: (param: Payload) => void) => () => void;
7
+ /**
8
+ * Subscribe to events with a weak reference tied to an owner object.
9
+ * The listener will be automatically removed when the owner is garbage collected.
10
+ * Use this to prevent memory leaks in class instances or services.
11
+ */
12
+ listenWeak: (owner: object, listener: (param: Payload) => void) => () => void;
13
+ name: string;
14
+ notify: (param: Payload) => void;
15
+ useEventListener: (listener: (param: Payload) => void, deps: ReadonlyArray<any>) => void;
16
+ };
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA0IA,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM;IAEnD;;;OAGG;uBACgB,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI;IAO3C;;;;OAIG;wBACiB,MAAM,YAAY,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI;;oBAS9C,OAAO;iCAKX,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,QAC5B,aAAa,CAAC,GAAG,CAAC;EAG7B"}
package/dist/index.js ADDED
@@ -0,0 +1,121 @@
1
+ import React, { useRef } from 'react';
2
+ // Global state
3
+ const listenersMap = new Map();
4
+ // FinalizationRegistry for proactive cleanup when owner objects are GC'd
5
+ const finalizationRegistry = new FinalizationRegistry(({ eventName, entry }) => {
6
+ const listeners = listenersMap.get(eventName);
7
+ if (listeners) {
8
+ listeners.delete(entry);
9
+ if (listeners.size === 0) {
10
+ listenersMap.delete(eventName);
11
+ }
12
+ }
13
+ });
14
+ function addListener(eventName, listener) {
15
+ let listeners = listenersMap.get(eventName);
16
+ if (!listeners) {
17
+ listeners = new Set();
18
+ listenersMap.set(eventName, listeners);
19
+ }
20
+ const entry = {
21
+ listener,
22
+ ownerRef: null, // Strong reference
23
+ };
24
+ listeners.add(entry);
25
+ return entry;
26
+ }
27
+ function addWeakListener(eventName, owner, listener) {
28
+ if (typeof owner !== 'object' || owner === null) {
29
+ throw new TypeError('Owner must be a non-null object');
30
+ }
31
+ let listeners = listenersMap.get(eventName);
32
+ if (!listeners) {
33
+ listeners = new Set();
34
+ listenersMap.set(eventName, listeners);
35
+ }
36
+ const entry = {
37
+ listener,
38
+ ownerRef: new WeakRef(owner),
39
+ };
40
+ listeners.add(entry);
41
+ finalizationRegistry.register(owner, { eventName, entry }, entry);
42
+ return entry;
43
+ }
44
+ function removeListenerEntry(eventName, entry) {
45
+ const listeners = listenersMap.get(eventName);
46
+ if (listeners) {
47
+ listeners.delete(entry);
48
+ if (entry.ownerRef !== null) {
49
+ finalizationRegistry.unregister(entry);
50
+ }
51
+ if (listeners.size === 0) {
52
+ listenersMap.delete(eventName);
53
+ }
54
+ }
55
+ }
56
+ function notify(eventName, ...params) {
57
+ const listeners = listenersMap.get(eventName);
58
+ if (!listeners)
59
+ return false;
60
+ const toRemove = [];
61
+ for (const entry of listeners) {
62
+ // Lazy cleanup: check if weak ref owner is still alive
63
+ if (entry.ownerRef !== null && entry.ownerRef.deref() === undefined) {
64
+ toRemove.push(entry);
65
+ continue;
66
+ }
67
+ entry.listener(...params);
68
+ }
69
+ // Remove dead weak refs
70
+ for (const entry of toRemove) {
71
+ listeners.delete(entry);
72
+ }
73
+ if (listeners.size === 0) {
74
+ listenersMap.delete(eventName);
75
+ }
76
+ return true;
77
+ }
78
+ function useEventNotifierListener(event, listener, deps) {
79
+ const local = useRef({ listener: listener });
80
+ local.current.listener = listener;
81
+ const callListener = React.useCallback((...params) => {
82
+ local.current.listener(...params);
83
+ }, []);
84
+ React.useEffect(() => {
85
+ const entry = addListener(event, callListener);
86
+ return () => {
87
+ removeListenerEntry(event, entry);
88
+ };
89
+ }, deps);
90
+ }
91
+ export function makeEventNotifier(name) {
92
+ return {
93
+ /**
94
+ * Subscribe to events with a strong reference.
95
+ * The listener will remain active until manually unsubscribed.
96
+ */
97
+ listen: (listener) => {
98
+ const entry = addListener(name, listener);
99
+ return () => {
100
+ removeListenerEntry(name, entry);
101
+ };
102
+ },
103
+ /**
104
+ * Subscribe to events with a weak reference tied to an owner object.
105
+ * The listener will be automatically removed when the owner is garbage collected.
106
+ * Use this to prevent memory leaks in class instances or services.
107
+ */
108
+ listenWeak: (owner, listener) => {
109
+ const entry = addWeakListener(name, owner, listener);
110
+ return () => {
111
+ removeListenerEntry(name, entry);
112
+ };
113
+ },
114
+ name: name,
115
+ notify: (param) => {
116
+ notify(name, param);
117
+ },
118
+ useEventListener: (listener, deps) => useEventNotifierListener(name, listener, deps),
119
+ };
120
+ }
121
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,107 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { renderHook } from '@testing-library/react';
4
+ import { makeEventNotifier } from './index.js';
5
+ describe('makeEventNotifier', () => {
6
+ it('should notify listeners when event is triggered', () => {
7
+ const eventName = 'test-event-1';
8
+ const notifier = makeEventNotifier(eventName);
9
+ const listener = vi.fn();
10
+ const cleanup = notifier.listen(listener);
11
+ notifier.notify('hello');
12
+ expect(listener).toHaveBeenCalledWith('hello');
13
+ expect(listener).toHaveBeenCalledTimes(1);
14
+ cleanup();
15
+ });
16
+ it('should allow unsubscribing', () => {
17
+ const eventName = 'test-event-2';
18
+ const notifier = makeEventNotifier(eventName);
19
+ const listener = vi.fn();
20
+ const cleanup = notifier.listen(listener);
21
+ cleanup();
22
+ notifier.notify('hello');
23
+ expect(listener).not.toHaveBeenCalled();
24
+ });
25
+ it('should handle multiple listeners', () => {
26
+ const eventName = 'test-event-3';
27
+ const notifier = makeEventNotifier(eventName);
28
+ const listenerOne = vi.fn();
29
+ const listenerTwo = vi.fn();
30
+ const cleanupOne = notifier.listen(listenerOne);
31
+ const cleanupTwo = notifier.listen(listenerTwo);
32
+ notifier.notify('hello');
33
+ expect(listenerOne).toHaveBeenCalledWith('hello');
34
+ expect(listenerTwo).toHaveBeenCalledWith('hello');
35
+ cleanupOne();
36
+ notifier.notify('world');
37
+ expect(listenerOne).toHaveBeenCalledTimes(1);
38
+ expect(listenerTwo).toHaveBeenCalledTimes(2);
39
+ expect(listenerTwo).toHaveBeenCalledWith('world');
40
+ cleanupTwo();
41
+ });
42
+ it('should work with useEventListener hook', () => {
43
+ const eventName = 'test-event-hook';
44
+ const notifier = makeEventNotifier(eventName);
45
+ const listener = vi.fn();
46
+ const { unmount } = renderHook(() => notifier.useEventListener(listener, []));
47
+ notifier.notify('from hook');
48
+ expect(listener).toHaveBeenCalledWith('from hook');
49
+ unmount();
50
+ notifier.notify('after unmount');
51
+ expect(listener).toHaveBeenCalledTimes(1);
52
+ });
53
+ });
54
+ describe('listenWeak', () => {
55
+ it('should notify weak listener when owner is alive', () => {
56
+ const notifier = makeEventNotifier('weak-test-1');
57
+ const listener = vi.fn();
58
+ const owner = { id: 'test-owner' };
59
+ notifier.listenWeak(owner, listener);
60
+ notifier.notify('hello');
61
+ expect(listener).toHaveBeenCalledWith('hello');
62
+ });
63
+ it('should allow manual unsubscribe of weak listener', () => {
64
+ const notifier = makeEventNotifier('weak-test-2');
65
+ const listener = vi.fn();
66
+ const owner = { id: 'test-owner' };
67
+ const unsubscribe = notifier.listenWeak(owner, listener);
68
+ unsubscribe();
69
+ notifier.notify('hello');
70
+ expect(listener).not.toHaveBeenCalled();
71
+ });
72
+ it('should throw if owner is not an object', () => {
73
+ const notifier = makeEventNotifier('weak-test-3');
74
+ const listener = vi.fn();
75
+ expect(() => {
76
+ // @ts-expect-error - testing runtime validation
77
+ notifier.listenWeak('not-an-object', listener);
78
+ }).toThrow(TypeError);
79
+ expect(() => {
80
+ // @ts-expect-error - testing runtime validation
81
+ notifier.listenWeak(null, listener);
82
+ }).toThrow(TypeError);
83
+ });
84
+ it('should support multiple weak listeners with same owner', () => {
85
+ const notifier = makeEventNotifier('weak-test-4');
86
+ const listener1 = vi.fn();
87
+ const listener2 = vi.fn();
88
+ const owner = { id: 'shared-owner' };
89
+ notifier.listenWeak(owner, listener1);
90
+ notifier.listenWeak(owner, listener2);
91
+ notifier.notify('hello');
92
+ expect(listener1).toHaveBeenCalledWith('hello');
93
+ expect(listener2).toHaveBeenCalledWith('hello');
94
+ });
95
+ it('should support mixed strong and weak listeners', () => {
96
+ const notifier = makeEventNotifier('weak-test-5');
97
+ const strongListener = vi.fn();
98
+ const weakListener = vi.fn();
99
+ const owner = { id: 'owner' };
100
+ notifier.listen(strongListener);
101
+ notifier.listenWeak(owner, weakListener);
102
+ notifier.notify('hello');
103
+ expect(strongListener).toHaveBeenCalledWith('hello');
104
+ expect(weakListener).toHaveBeenCalledWith('hello');
105
+ });
106
+ });
107
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@therms/react-event-bus",
3
+ "version": "1.0.1",
4
+ "description": "An event-bus for use in React applications",
5
+ "keywords": [
6
+ "react",
7
+ "event-bus",
8
+ "event-hooks",
9
+ "react-notifier",
10
+ "react-native"
11
+ ],
12
+ "files": [
13
+ "dist",
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "homepage": "https://bitbucket.org/thermsio/react-event-bus#readme",
18
+ "bugs": {
19
+ "url": "https://bitbucket.org/thermsio/react-event-bus/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://bitbucket.org/thermsio/react-event-bus.git"
24
+ },
25
+ "license": "MIT",
26
+ "author": "coryrobinson42@gmail.com",
27
+ "type": "module",
28
+ "main": "dist/index.js",
29
+ "types": "dist/index.d.ts",
30
+ "exports": {
31
+ ".": "./dist/index.js"
32
+ },
33
+ "scripts": {
34
+ "test": "vitest run",
35
+ "test:watch": "vitest",
36
+ "build": "tsc",
37
+ "prepublishOnly": "tsc"
38
+ },
39
+ "devDependencies": {
40
+ "@testing-library/react": "^16.3.0",
41
+ "@types/react": "^19.2.7",
42
+ "happy-dom": "^20.0.11",
43
+ "react": "^19.2.2",
44
+ "typescript": "^5.9.3",
45
+ "vite": "^7.2.7",
46
+ "vitest": "^4.0.15"
47
+ }
48
+ }
@@ -0,0 +1,143 @@
1
+ // @vitest-environment happy-dom
2
+ import {describe, it, expect, vi} from 'vitest'
3
+ import {renderHook} from '@testing-library/react'
4
+ import {makeEventNotifier} from './index.js'
5
+
6
+ describe('makeEventNotifier', () => {
7
+ it('should notify listeners when event is triggered', () => {
8
+ const eventName = 'test-event-1'
9
+ const notifier = makeEventNotifier<string>(eventName)
10
+ const listener = vi.fn()
11
+
12
+ const cleanup = notifier.listen(listener)
13
+
14
+ notifier.notify('hello')
15
+
16
+ expect(listener).toHaveBeenCalledWith('hello')
17
+ expect(listener).toHaveBeenCalledTimes(1)
18
+
19
+ cleanup()
20
+ })
21
+
22
+ it('should allow unsubscribing', () => {
23
+ const eventName = 'test-event-2'
24
+ const notifier = makeEventNotifier<string>(eventName)
25
+ const listener = vi.fn()
26
+
27
+ const cleanup = notifier.listen(listener)
28
+ cleanup()
29
+
30
+ notifier.notify('hello')
31
+
32
+ expect(listener).not.toHaveBeenCalled()
33
+ })
34
+
35
+ it('should handle multiple listeners', () => {
36
+ const eventName = 'test-event-3'
37
+ const notifier = makeEventNotifier<string>(eventName)
38
+ const listenerOne = vi.fn()
39
+ const listenerTwo = vi.fn()
40
+
41
+ const cleanupOne = notifier.listen(listenerOne)
42
+ const cleanupTwo = notifier.listen(listenerTwo)
43
+
44
+ notifier.notify('hello')
45
+
46
+ expect(listenerOne).toHaveBeenCalledWith('hello')
47
+ expect(listenerTwo).toHaveBeenCalledWith('hello')
48
+
49
+ cleanupOne()
50
+
51
+ notifier.notify('world')
52
+
53
+ expect(listenerOne).toHaveBeenCalledTimes(1)
54
+ expect(listenerTwo).toHaveBeenCalledTimes(2)
55
+ expect(listenerTwo).toHaveBeenCalledWith('world')
56
+
57
+ cleanupTwo()
58
+ })
59
+
60
+ it('should work with useEventListener hook', () => {
61
+ const eventName = 'test-event-hook'
62
+ const notifier = makeEventNotifier<string>(eventName)
63
+ const listener = vi.fn()
64
+
65
+ const {unmount} = renderHook(() => notifier.useEventListener(listener, []))
66
+
67
+ notifier.notify('from hook')
68
+ expect(listener).toHaveBeenCalledWith('from hook')
69
+
70
+ unmount()
71
+
72
+ notifier.notify('after unmount')
73
+ expect(listener).toHaveBeenCalledTimes(1)
74
+ })
75
+ })
76
+
77
+ describe('listenWeak', () => {
78
+ it('should notify weak listener when owner is alive', () => {
79
+ const notifier = makeEventNotifier<string>('weak-test-1')
80
+ const listener = vi.fn()
81
+ const owner = {id: 'test-owner'}
82
+
83
+ notifier.listenWeak(owner, listener)
84
+ notifier.notify('hello')
85
+
86
+ expect(listener).toHaveBeenCalledWith('hello')
87
+ })
88
+
89
+ it('should allow manual unsubscribe of weak listener', () => {
90
+ const notifier = makeEventNotifier<string>('weak-test-2')
91
+ const listener = vi.fn()
92
+ const owner = {id: 'test-owner'}
93
+
94
+ const unsubscribe = notifier.listenWeak(owner, listener)
95
+ unsubscribe()
96
+ notifier.notify('hello')
97
+
98
+ expect(listener).not.toHaveBeenCalled()
99
+ })
100
+
101
+ it('should throw if owner is not an object', () => {
102
+ const notifier = makeEventNotifier<string>('weak-test-3')
103
+ const listener = vi.fn()
104
+
105
+ expect(() => {
106
+ // @ts-expect-error - testing runtime validation
107
+ notifier.listenWeak('not-an-object', listener)
108
+ }).toThrow(TypeError)
109
+
110
+ expect(() => {
111
+ // @ts-expect-error - testing runtime validation
112
+ notifier.listenWeak(null, listener)
113
+ }).toThrow(TypeError)
114
+ })
115
+
116
+ it('should support multiple weak listeners with same owner', () => {
117
+ const notifier = makeEventNotifier<string>('weak-test-4')
118
+ const listener1 = vi.fn()
119
+ const listener2 = vi.fn()
120
+ const owner = {id: 'shared-owner'}
121
+
122
+ notifier.listenWeak(owner, listener1)
123
+ notifier.listenWeak(owner, listener2)
124
+ notifier.notify('hello')
125
+
126
+ expect(listener1).toHaveBeenCalledWith('hello')
127
+ expect(listener2).toHaveBeenCalledWith('hello')
128
+ })
129
+
130
+ it('should support mixed strong and weak listeners', () => {
131
+ const notifier = makeEventNotifier<string>('weak-test-5')
132
+ const strongListener = vi.fn()
133
+ const weakListener = vi.fn()
134
+ const owner = {id: 'owner'}
135
+
136
+ notifier.listen(strongListener)
137
+ notifier.listenWeak(owner, weakListener)
138
+ notifier.notify('hello')
139
+
140
+ expect(strongListener).toHaveBeenCalledWith('hello')
141
+ expect(weakListener).toHaveBeenCalledWith('hello')
142
+ })
143
+ })
package/src/index.ts ADDED
@@ -0,0 +1,176 @@
1
+ import React, {useRef} from 'react'
2
+
3
+ /**
4
+ * A simplified version of https://github.com/primus/eventemitter3
5
+ *
6
+ * Supports both strong and weak references via listen() and listenWeak().
7
+ * Use listenWeak(owner, listener) to tie listener lifetime to an owner object,
8
+ * preventing memory leaks when the owner is garbage collected.
9
+ */
10
+
11
+ // Types
12
+ type ListenerFn = (...params: any[]) => void
13
+
14
+ type ListenerEntry = {
15
+ listener: ListenerFn
16
+ ownerRef: WeakRef<object> | null // null = strong reference
17
+ }
18
+
19
+ // Global state
20
+ const listenersMap: Map<string, Set<ListenerEntry>> = new Map()
21
+
22
+ // FinalizationRegistry for proactive cleanup when owner objects are GC'd
23
+ const finalizationRegistry = new FinalizationRegistry<{
24
+ eventName: string
25
+ entry: ListenerEntry
26
+ }>(({eventName, entry}) => {
27
+ const listeners = listenersMap.get(eventName)
28
+ if (listeners) {
29
+ listeners.delete(entry)
30
+ if (listeners.size === 0) {
31
+ listenersMap.delete(eventName)
32
+ }
33
+ }
34
+ })
35
+
36
+ function addListener(eventName: string, listener: ListenerFn): ListenerEntry {
37
+ let listeners = listenersMap.get(eventName)
38
+ if (!listeners) {
39
+ listeners = new Set()
40
+ listenersMap.set(eventName, listeners)
41
+ }
42
+
43
+ const entry: ListenerEntry = {
44
+ listener,
45
+ ownerRef: null, // Strong reference
46
+ }
47
+
48
+ listeners.add(entry)
49
+ return entry
50
+ }
51
+
52
+ function addWeakListener(
53
+ eventName: string,
54
+ owner: object,
55
+ listener: ListenerFn,
56
+ ): ListenerEntry {
57
+ if (typeof owner !== 'object' || owner === null) {
58
+ throw new TypeError('Owner must be a non-null object')
59
+ }
60
+
61
+ let listeners = listenersMap.get(eventName)
62
+ if (!listeners) {
63
+ listeners = new Set()
64
+ listenersMap.set(eventName, listeners)
65
+ }
66
+
67
+ const entry: ListenerEntry = {
68
+ listener,
69
+ ownerRef: new WeakRef(owner),
70
+ }
71
+
72
+ listeners.add(entry)
73
+ finalizationRegistry.register(owner, {eventName, entry}, entry)
74
+
75
+ return entry
76
+ }
77
+
78
+ function removeListenerEntry(eventName: string, entry: ListenerEntry): void {
79
+ const listeners = listenersMap.get(eventName)
80
+ if (listeners) {
81
+ listeners.delete(entry)
82
+ if (entry.ownerRef !== null) {
83
+ finalizationRegistry.unregister(entry)
84
+ }
85
+ if (listeners.size === 0) {
86
+ listenersMap.delete(eventName)
87
+ }
88
+ }
89
+ }
90
+
91
+ function notify<T = any>(eventName: string, ...params: T[]): boolean {
92
+ const listeners = listenersMap.get(eventName)
93
+ if (!listeners) return false
94
+
95
+ const toRemove: ListenerEntry[] = []
96
+
97
+ for (const entry of listeners) {
98
+ // Lazy cleanup: check if weak ref owner is still alive
99
+ if (entry.ownerRef !== null && entry.ownerRef.deref() === undefined) {
100
+ toRemove.push(entry)
101
+ continue
102
+ }
103
+
104
+ entry.listener(...params)
105
+ }
106
+
107
+ // Remove dead weak refs
108
+ for (const entry of toRemove) {
109
+ listeners.delete(entry)
110
+ }
111
+
112
+ if (listeners.size === 0) {
113
+ listenersMap.delete(eventName)
114
+ }
115
+
116
+ return true
117
+ }
118
+
119
+ function useEventNotifierListener<T extends (...params: any) => void>(
120
+ event: string,
121
+ listener: T,
122
+ deps: ReadonlyArray<any>,
123
+ ) {
124
+ const local = useRef<{listener: T}>({listener: listener})
125
+ local.current.listener = listener
126
+
127
+ const callListener = React.useCallback((...params: Parameters<T>) => {
128
+ local.current.listener(...params)
129
+ }, [])
130
+
131
+ React.useEffect(() => {
132
+ const entry = addListener(event, callListener)
133
+ return () => {
134
+ removeListenerEntry(event, entry)
135
+ }
136
+ }, deps)
137
+ }
138
+
139
+ export function makeEventNotifier<Payload>(name: string) {
140
+ return {
141
+ /**
142
+ * Subscribe to events with a strong reference.
143
+ * The listener will remain active until manually unsubscribed.
144
+ */
145
+ listen: (listener: (param: Payload) => void) => {
146
+ const entry = addListener(name, listener)
147
+ return () => {
148
+ removeListenerEntry(name, entry)
149
+ }
150
+ },
151
+
152
+ /**
153
+ * Subscribe to events with a weak reference tied to an owner object.
154
+ * The listener will be automatically removed when the owner is garbage collected.
155
+ * Use this to prevent memory leaks in class instances or services.
156
+ */
157
+ listenWeak: (owner: object, listener: (param: Payload) => void) => {
158
+ const entry = addWeakListener(name, owner, listener)
159
+ return () => {
160
+ removeListenerEntry(name, entry)
161
+ }
162
+ },
163
+
164
+ name: name,
165
+
166
+ notify: (param: Payload) => {
167
+ notify(name, param)
168
+ },
169
+
170
+ useEventListener: (
171
+ listener: (param: Payload) => void,
172
+ deps: ReadonlyArray<any>,
173
+ ) => useEventNotifierListener(name, listener, deps),
174
+ }
175
+ }
176
+