@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 +94 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +121 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +107 -0
- package/dist/index.test.js.map +1 -0
- package/package.json +48 -0
- package/src/index.test.ts +143 -0
- package/src/index.ts +176 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
|