@siggn/react 0.0.1 → 0.1.0
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/CHANGELOG.md +16 -0
- package/README.md +104 -133
- package/package.json +3 -2
- package/src/hooks.ts +85 -9
- package/src/types.ts +8 -0
- package/tests/hooks.test.tsx +219 -4
- package/tsconfig.test.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @siggn/react
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`54e3afd`](https://github.com/Guiguerreiro39/siggn/commit/54e3afd430bdb73ef0549ebdfbf9fc6f76bc62b1)
|
|
8
|
+
Thanks [@Guiguerreiro39](https://github.com/Guiguerreiro39)! - - Introduced automatic garbage
|
|
9
|
+
collection to prevent memory leaks.
|
|
10
|
+
- Added code documentation
|
|
11
|
+
- Added `subscriptionsCount` property to `Siggn`
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies
|
|
16
|
+
[[`54e3afd`](https://github.com/Guiguerreiro39/siggn/commit/54e3afd430bdb73ef0549ebdfbf9fc6f76bc62b1)]:
|
|
17
|
+
- @siggn/core@0.1.0
|
|
18
|
+
|
|
3
19
|
## 0.0.1
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,202 +1,173 @@
|
|
|
1
|
-
|
|
1
|
+
[](https://www.npmjs.com/package/%40siggn%2Freact)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# @siggn/react
|
|
4
|
+
|
|
5
|
+
React package for `@siggn/core`, providing a simple and idiomatic way to integrate the message bus with your React components.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
|
-
- **
|
|
8
|
-
|
|
9
|
-
- **
|
|
10
|
-
- **Simple API**: Easy to learn and use, with a clear and concise API.
|
|
9
|
+
- **Seamless Integration**: Hooks-based API that feels natural in React.
|
|
10
|
+
- **Automatic Cleanup**: Subscriptions are automatically managed throughout the component lifecycle.
|
|
11
|
+
- **Type-Safe**: Full TypeScript support, inheriting the type safety of `@siggn/core`.
|
|
11
12
|
|
|
12
13
|
## Installation
|
|
13
14
|
|
|
14
|
-
You can install the package using your favorite package manager:
|
|
15
|
-
|
|
16
15
|
```bash
|
|
17
|
-
npm install @siggn/
|
|
16
|
+
npm install @siggn/react
|
|
18
17
|
```
|
|
19
18
|
|
|
20
19
|
```bash
|
|
21
|
-
yarn add @siggn/
|
|
20
|
+
yarn add @siggn/react
|
|
22
21
|
```
|
|
23
22
|
|
|
24
23
|
```bash
|
|
25
|
-
pnpm add @siggn/
|
|
24
|
+
pnpm add @siggn/react
|
|
26
25
|
```
|
|
27
26
|
|
|
28
27
|
## Usage
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
The primary way to use `@siggn/react` is by creating a `Siggn` instance and sharing it across your application. You can do this using React Context or by exporting a singleton instance.
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
import { Siggn } from '@siggn/core';
|
|
31
|
+
### 1. Create a Siggn Instance
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
type Message =
|
|
37
|
-
| { type: 'user_created'; userId: string; name: string }
|
|
38
|
-
| { type: 'user_deleted'; userId: string };
|
|
33
|
+
It's recommended to create a single `Siggn` instance and share it throughout your app.
|
|
39
34
|
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
```typescript
|
|
36
|
+
// src/siggn.ts
|
|
37
|
+
import { Siggn } from '@siggn/react';
|
|
42
38
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
// Define your message types
|
|
40
|
+
export type Message =
|
|
41
|
+
| { type: 'user_login'; name: string }
|
|
42
|
+
| { type: 'user_logout' };
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
// Create and export the instance
|
|
45
|
+
export const siggn = new Siggn<Message>();
|
|
46
|
+
```
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
siggn.publish({ type: 'user_created', userId: '123', name: 'John Doe' });
|
|
53
|
-
// Output: [Analytics] New user created: John Doe (ID: 123)
|
|
48
|
+
Alternatively, you can use the `useSiggn` hook to create a `Siggn` instance that is scoped to a component and its children.
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
siggn.unsubscribe(subscriberId);
|
|
50
|
+
### 2. Subscribe to Events in a Component
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
// No output, because the subscriber was removed.
|
|
60
|
-
```
|
|
52
|
+
Use the `useSubscribe` hook to listen for messages. It automatically handles subscribing and unsubscribing.
|
|
61
53
|
|
|
62
|
-
|
|
54
|
+
```tsx
|
|
55
|
+
// src/components/Notification.tsx
|
|
56
|
+
import { useState } from 'react';
|
|
57
|
+
import { useSubscribe } from '@siggn/react';
|
|
58
|
+
import { siggn, type Message } from '../siggn';
|
|
63
59
|
|
|
64
|
-
|
|
60
|
+
function Notification() {
|
|
61
|
+
const [notification, setNotification] = useState<string | null>(null);
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
useSubscribe(siggn, (subscribe) => {
|
|
64
|
+
subscribe('user_login', (msg) => {
|
|
65
|
+
setNotification(`Welcome, ${msg.name}!`);
|
|
66
|
+
});
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
68
|
+
subscribe('user_logout', () => {
|
|
69
|
+
setNotification('You have been logged out.');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
if (!notification) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
return <div className='notification'>{notification}</div>;
|
|
78
|
+
}
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
###
|
|
81
|
-
|
|
82
|
-
You can use `subscribeMany` to group subscriptions for a single subscriber.
|
|
81
|
+
### 3. Publish Events
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
const auditService = siggn.make('audit-service');
|
|
83
|
+
You can publish events from anywhere in your application.
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
subscribe('user_deleted', (msg) => {
|
|
92
|
-
console.log(`[Audit] User deleted: ${msg.userId}`);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
85
|
+
```tsx
|
|
86
|
+
// src/components/AuthButton.tsx
|
|
87
|
+
import { siggn } from '../siggn';
|
|
95
88
|
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
function AuthButton({ isLoggedIn }: { isLoggedIn: boolean }) {
|
|
90
|
+
const handleClick = () => {
|
|
91
|
+
if (isLoggedIn) {
|
|
92
|
+
siggn.publish({ type: 'user_logout' });
|
|
93
|
+
} else {
|
|
94
|
+
siggn.publish({ type: 'user_login', name: 'Jane Doe' });
|
|
95
|
+
}
|
|
96
|
+
};
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
return <button onClick={handleClick}>{isLoggedIn ? 'Log Out' : 'Log In'}</button>;
|
|
99
|
+
}
|
|
101
100
|
```
|
|
102
101
|
|
|
103
|
-
### Subscribing to
|
|
104
|
-
|
|
105
|
-
If you need to listen to every message that passes through the bus, regardless of its type, you can
|
|
106
|
-
use `subscribeAll`. This is useful for cross-cutting concerns like logging or debugging.
|
|
102
|
+
### Subscribing to All Events
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
const logger = siggn.make('logger-service');
|
|
110
|
-
|
|
111
|
-
logger.subscribeAll((msg) => {
|
|
112
|
-
console.log(`[Logger] Received event of type: ${msg.type}`);
|
|
113
|
-
});
|
|
104
|
+
If you need to listen to all messages, you can use `useSubscribeAll`. This is useful for cross-cutting concerns like logging or analytics.
|
|
114
105
|
|
|
115
|
-
|
|
116
|
-
//
|
|
106
|
+
```tsx
|
|
107
|
+
// src/components/Logger.tsx
|
|
108
|
+
import { useSubscribeAll } from '@siggn/react';
|
|
109
|
+
import { siggn } from '../siggn';
|
|
117
110
|
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
function Logger() {
|
|
112
|
+
useSubscribeAll(siggn, (msg) => {
|
|
113
|
+
console.log(`[Logger] Event of type ${msg.type} was triggered`);
|
|
114
|
+
});
|
|
120
115
|
|
|
121
|
-
//
|
|
122
|
-
|
|
116
|
+
return null; // This component does not render anything
|
|
117
|
+
}
|
|
123
118
|
```
|
|
124
119
|
|
|
125
|
-
###
|
|
120
|
+
### Using `useSiggn`
|
|
126
121
|
|
|
127
|
-
The `
|
|
128
|
-
message types of its parent. This is useful for creating specialized message buses that extend a
|
|
129
|
-
base set of events without affecting the parent bus.
|
|
122
|
+
The `useSiggn` hook creates a `Siggn` instance that is tied to the component's lifecycle. This can be useful for local, component-specific event buses.
|
|
130
123
|
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
const baseSiggn = new Siggn<Message>();
|
|
124
|
+
```tsx
|
|
125
|
+
import { useSiggn, useSubscribe } from '@siggn/react';
|
|
134
126
|
|
|
135
|
-
|
|
136
|
-
type AdminMessage = { type: 'admin_login'; adminId: string };
|
|
127
|
+
type LocalMessage = { type: 'local_event' };
|
|
137
128
|
|
|
138
|
-
|
|
139
|
-
const
|
|
129
|
+
function LocalComponent() {
|
|
130
|
+
const localSiggn = useSiggn<LocalMessage>();
|
|
140
131
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
adminSiggn.subscribe('auth-service', 'admin_login', (msg) => {
|
|
147
|
-
console.log(`[Admin Auth] Admin logged in: ${msg.adminId}`);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// 4. Publish events on the child bus
|
|
151
|
-
adminSiggn.publish({ type: 'user_created', userId: 'abc', name: 'Alice' });
|
|
152
|
-
// Output: [Admin Audit] User created: Alice
|
|
132
|
+
useSubscribe(localSiggn, (subscribe) => {
|
|
133
|
+
subscribe('local_event', () => {
|
|
134
|
+
console.log('Local event received!');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
153
137
|
|
|
154
|
-
|
|
155
|
-
|
|
138
|
+
const triggerEvent = () => {
|
|
139
|
+
localSiggn.publish({ type: 'local_event' });
|
|
140
|
+
};
|
|
156
141
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
baseSiggn.publish({ type: 'user_created', userId: 'def', name: 'Bob' });
|
|
160
|
-
// No output, because the subscription is on `adminSiggn`.
|
|
142
|
+
return <button onClick={triggerEvent}>Trigger Local Event</button>;
|
|
143
|
+
}
|
|
161
144
|
```
|
|
162
145
|
|
|
163
146
|
## API
|
|
164
147
|
|
|
165
|
-
### `
|
|
166
|
-
|
|
167
|
-
Creates a new message bus instance. `T` is a union type of all possible messages.
|
|
168
|
-
|
|
169
|
-
### `publish(msg)`
|
|
170
|
-
|
|
171
|
-
Publishes a message to all relevant subscribers.
|
|
172
|
-
|
|
173
|
-
### `subscribe(id, type, callback)`
|
|
174
|
-
|
|
175
|
-
Subscribes a callback to a specific message type with a unique subscriber ID.
|
|
176
|
-
|
|
177
|
-
### `subscribeAll(id, callback)`
|
|
148
|
+
### `useSiggn<T>()`
|
|
178
149
|
|
|
179
|
-
|
|
180
|
-
every message published on the bus.
|
|
150
|
+
Creates and returns a `Siggn` instance that persists for the lifetime of the component.
|
|
181
151
|
|
|
182
|
-
|
|
152
|
+
- `T`: A union type of all possible messages.
|
|
183
153
|
|
|
184
|
-
|
|
154
|
+
Returns a `Siggn<T>` instance.
|
|
185
155
|
|
|
186
|
-
### `
|
|
156
|
+
### `useSubscribe(options, setup, deps)`
|
|
187
157
|
|
|
188
|
-
|
|
189
|
-
pre-bound to the provided ID. This is useful for encapsulating subscription logic within a component
|
|
190
|
-
or service.
|
|
158
|
+
Subscribes to messages and automatically unsubscribes when the component unmounts.
|
|
191
159
|
|
|
192
|
-
|
|
160
|
+
- `options`: A `Siggn` instance or an object `{ instance: Siggn<T>; id?: string; }`.
|
|
161
|
+
- `setup`: A function that receives a `subscribe` helper to define subscriptions, similar to `subscribeMany` in `@siggn/core`.
|
|
162
|
+
- `deps` (optional): A dependency array to control when the subscriptions are re-created.
|
|
193
163
|
|
|
194
|
-
|
|
164
|
+
### `useSubscribeAll(options, callback, deps)`
|
|
195
165
|
|
|
196
|
-
|
|
166
|
+
Subscribes to all messages and automatically unsubscribes when the component unmounts.
|
|
197
167
|
|
|
198
|
-
|
|
199
|
-
|
|
168
|
+
- `options`: A `Siggn` instance or an object `{ instance: Siggn<T>; id?: string; }`.
|
|
169
|
+
- `callback`: A function that will be called with every message.
|
|
170
|
+
- `deps` (optional): A dependency array to control when the subscriptions are re-created.
|
|
200
171
|
|
|
201
172
|
## License
|
|
202
173
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siggn/react",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A lightweight type safe message bus system for React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pub/sub",
|
|
@@ -55,13 +55,14 @@
|
|
|
55
55
|
"react": ">=18 <20"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@siggn/core": "0.0
|
|
58
|
+
"@siggn/core": "0.1.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"dev": "vite build --watch",
|
|
62
62
|
"clean": "rm -rf dist",
|
|
63
63
|
"test:watch": "vitest",
|
|
64
64
|
"test": "vitest run",
|
|
65
|
+
"coverage": "vitest run --coverage",
|
|
65
66
|
"build": "vite build"
|
|
66
67
|
}
|
|
67
68
|
}
|
package/src/hooks.ts
CHANGED
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
import { type Msg, Siggn } from '@siggn/core';
|
|
2
|
+
import type { SubscriptionOptions } from 'packages/react/src/types';
|
|
2
3
|
import { useEffect, useMemo, useRef, type DependencyList } from 'react';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Creates and returns a `Siggn` instance that persists for the lifetime of the component.
|
|
7
|
+
* This is useful for creating a message bus scoped to a component and its children.
|
|
8
|
+
*
|
|
9
|
+
* @template T A union of all possible message types for the new instance.
|
|
10
|
+
* @returns A `Siggn<T>` instance.
|
|
11
|
+
* @category Lifecycle
|
|
12
|
+
* @since 0.0.1
|
|
13
|
+
* @example
|
|
14
|
+
*
|
|
15
|
+
```tsx
|
|
16
|
+
* function MyComponent() {
|
|
17
|
+
* const localSiggn = useSiggn<{ type: 'local-event' }>();
|
|
18
|
+
* // ...
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function useSiggn<T extends Msg>(): Siggn<T> {
|
|
23
|
+
const siggn = useRef(new Siggn<T>());
|
|
24
|
+
return siggn.current;
|
|
25
|
+
}
|
|
10
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Subscribes to messages and automatically unsubscribes when the component unmounts.
|
|
29
|
+
*
|
|
30
|
+
* @template T A union of all possible message types.
|
|
31
|
+
* @param options A `Siggn` instance or an object with the instance and an optional subscriber ID.
|
|
32
|
+
* @param setup A function that receives a `subscribe` helper to define subscriptions.
|
|
33
|
+
* @param deps An optional dependency array to control when the subscriptions are re-created.
|
|
34
|
+
* @category Subscription
|
|
35
|
+
* @since 0.0.1
|
|
36
|
+
* @example
|
|
37
|
+
*
|
|
38
|
+
```tsx
|
|
39
|
+
* import { siggn } from './siggn'; // Your shared instance
|
|
40
|
+
*
|
|
41
|
+
* function MyComponent() {
|
|
42
|
+
* useSubscribe(siggn, (subscribe) => {
|
|
43
|
+
* subscribe('user-created', (msg) => console.log(msg.name));
|
|
44
|
+
* });
|
|
45
|
+
* // ...
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
11
49
|
export function useSubscribe<T extends Msg>(
|
|
12
50
|
options: SubscriptionOptions<T>,
|
|
13
51
|
setup: (
|
|
@@ -33,7 +71,45 @@ export function useSubscribe<T extends Msg>(
|
|
|
33
71
|
}, [instance, id, ...deps]);
|
|
34
72
|
}
|
|
35
73
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Subscribes to all messages on a `Siggn` instance and automatically unsubscribes
|
|
76
|
+
* when the component unmounts.
|
|
77
|
+
*
|
|
78
|
+
* @template T A union of all possible message types.
|
|
79
|
+
* @param options A `Siggn` instance or an object with the instance and an optional subscriber ID.
|
|
80
|
+
* @param callback The function to call for any message.
|
|
81
|
+
* @param deps An optional dependency array to control when the subscription is re-created.
|
|
82
|
+
* @category Subscription
|
|
83
|
+
* @since 0.0.1
|
|
84
|
+
* @example
|
|
85
|
+
*
|
|
86
|
+
```tsx
|
|
87
|
+
* import { siggn } from './siggn';
|
|
88
|
+
*
|
|
89
|
+
* function LoggerComponent() {
|
|
90
|
+
* useSubscribeAll(siggn, (msg) => {
|
|
91
|
+
* console.log(`[LOG]: ${msg.type}`);
|
|
92
|
+
* }, []);
|
|
93
|
+
* // ...
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export function useSubscribeAll<T extends Msg>(
|
|
98
|
+
options: SubscriptionOptions<T>,
|
|
99
|
+
callback: (msg: T) => void,
|
|
100
|
+
deps: DependencyList = [],
|
|
101
|
+
) {
|
|
102
|
+
const instance = useMemo(
|
|
103
|
+
() => (options instanceof Siggn ? options : options.instance),
|
|
104
|
+
[options],
|
|
105
|
+
);
|
|
106
|
+
const id = useMemo(() => instance.makeId('id' in options ? options.id : undefined), [instance]);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
instance.subscribeAll(id, callback);
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
instance.unsubscribeGlobal(id);
|
|
113
|
+
};
|
|
114
|
+
}, [instance, id, ...deps]);
|
|
39
115
|
}
|
package/src/types.ts
ADDED
package/tests/hooks.test.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { test, expect, describe, beforeEach } from 'vitest';
|
|
2
|
-
import { useSiggn, useSubscribe } from '../src/hooks.js';
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { useSiggn, useSubscribe, useSubscribeAll } from '../src/hooks.js';
|
|
3
3
|
import { Siggn } from '@siggn/core';
|
|
4
|
-
import { act, render, renderHook, screen } from '@testing-library/react';
|
|
4
|
+
import { act, render, renderHook, screen, cleanup } from '@testing-library/react';
|
|
5
5
|
import { useState } from 'react';
|
|
6
6
|
|
|
7
7
|
type Msg =
|
|
@@ -9,7 +9,9 @@ type Msg =
|
|
|
9
9
|
type: 'increment_count';
|
|
10
10
|
value: number;
|
|
11
11
|
}
|
|
12
|
-
| { type: 'decrement_count'; value: number }
|
|
12
|
+
| { type: 'decrement_count'; value: number }
|
|
13
|
+
| { type: 'custom_event'; data: string }
|
|
14
|
+
| { type: 'dependency_changed'; newValue: number };
|
|
13
15
|
|
|
14
16
|
describe('@siggn/react', () => {
|
|
15
17
|
let siggn: Siggn<Msg>;
|
|
@@ -19,6 +21,10 @@ describe('@siggn/react', () => {
|
|
|
19
21
|
siggn = result.current;
|
|
20
22
|
});
|
|
21
23
|
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
cleanup();
|
|
26
|
+
});
|
|
27
|
+
|
|
22
28
|
test('user should be able to create a siggn instance and subscribe using hooks', () => {
|
|
23
29
|
function TestComponent() {
|
|
24
30
|
const [count, setCount] = useState(0);
|
|
@@ -52,4 +58,213 @@ describe('@siggn/react', () => {
|
|
|
52
58
|
|
|
53
59
|
expect(screen.getByTestId('value')).toHaveTextContent('2');
|
|
54
60
|
});
|
|
61
|
+
|
|
62
|
+
test('useSiggn should return a stable instance across re-renders', () => {
|
|
63
|
+
const { result, rerender } = renderHook(() => useSiggn<Msg>());
|
|
64
|
+
const firstInstance = result.current;
|
|
65
|
+
|
|
66
|
+
rerender(); // Re-render the hook
|
|
67
|
+
|
|
68
|
+
const secondInstance = result.current;
|
|
69
|
+
expect(firstInstance).toBe(secondInstance); // Should be the same instance
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('useSubscribe should work with explicit id in options', () => {
|
|
73
|
+
let receivedData: string | null = null;
|
|
74
|
+
const customId = 'my-custom-subscriber';
|
|
75
|
+
|
|
76
|
+
function TestComponent() {
|
|
77
|
+
useSubscribe({ instance: siggn, id: customId }, (subscribe) => {
|
|
78
|
+
subscribe('custom_event', (msg) => {
|
|
79
|
+
receivedData = msg.data;
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
render(<TestComponent />);
|
|
86
|
+
|
|
87
|
+
act(() => {
|
|
88
|
+
siggn.publish({ type: 'custom_event', data: 'hello' });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(receivedData).toBe('hello');
|
|
92
|
+
|
|
93
|
+
// Manually unsubscribe using the custom ID
|
|
94
|
+
act(() => {
|
|
95
|
+
siggn.unsubscribe(customId);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
act(() => {
|
|
99
|
+
siggn.publish({ type: 'custom_event', data: 'world' });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Should not receive 'world' as it's unsubscribed
|
|
103
|
+
expect(receivedData).toBe('hello');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('useSubscribe should re-subscribe when dependencies change', () => {
|
|
107
|
+
let callCount = 0;
|
|
108
|
+
let lastReceivedValue = 0;
|
|
109
|
+
|
|
110
|
+
function TestComponent({ depValue }: { depValue: number }) {
|
|
111
|
+
useSubscribe(
|
|
112
|
+
siggn,
|
|
113
|
+
(subscribe) => {
|
|
114
|
+
callCount++; // This should increment when subscription is re-established
|
|
115
|
+
subscribe('dependency_changed', (msg) => {
|
|
116
|
+
lastReceivedValue = msg.newValue + depValue; // Use depValue in callback
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
[depValue], // Dependency array includes depValue
|
|
120
|
+
);
|
|
121
|
+
return <div data-testid='last-value'>{lastReceivedValue}</div>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { rerender } = render(<TestComponent depValue={10} />);
|
|
125
|
+
|
|
126
|
+
act(() => {
|
|
127
|
+
siggn.publish({ type: 'dependency_changed', newValue: 5 });
|
|
128
|
+
});
|
|
129
|
+
expect(lastReceivedValue).toBe(15); // 5 + 10
|
|
130
|
+
expect(callCount).toBe(1);
|
|
131
|
+
|
|
132
|
+
rerender(<TestComponent depValue={20} />); // Change dependency
|
|
133
|
+
|
|
134
|
+
act(() => {
|
|
135
|
+
siggn.publish({ type: 'dependency_changed', newValue: 5 });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(lastReceivedValue).toBe(25); // 5 + 20
|
|
139
|
+
expect(callCount).toBe(2); // Subscription should have been re-established
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('useSubscribe should automatically unsubscribe on component unmount', () => {
|
|
143
|
+
let receivedMessage: string | null = null;
|
|
144
|
+
|
|
145
|
+
function TestComponent() {
|
|
146
|
+
useSubscribe(siggn, (subscribe) => {
|
|
147
|
+
subscribe('custom_event', (msg) => {
|
|
148
|
+
receivedMessage = msg.data;
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const { unmount } = render(<TestComponent />);
|
|
155
|
+
|
|
156
|
+
act(() => {
|
|
157
|
+
siggn.publish({ type: 'custom_event', data: 'first message' });
|
|
158
|
+
});
|
|
159
|
+
expect(receivedMessage).toBe('first message');
|
|
160
|
+
|
|
161
|
+
receivedMessage = null; // Reset for next check
|
|
162
|
+
|
|
163
|
+
unmount(); // Unmount the component
|
|
164
|
+
|
|
165
|
+
act(() => {
|
|
166
|
+
siggn.publish({ type: 'custom_event', data: 'second message' });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// After unmount, the subscription should be gone
|
|
170
|
+
expect(receivedMessage).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('useSubscribeAll should receive all messages', () => {
|
|
174
|
+
const receivedMessages: Msg[] = [];
|
|
175
|
+
const callback = vi.fn((msg: Msg) => {
|
|
176
|
+
receivedMessages.push(msg);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
function TestComponent() {
|
|
180
|
+
useSubscribeAll(siggn, callback);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
render(<TestComponent />);
|
|
185
|
+
|
|
186
|
+
act(() => {
|
|
187
|
+
siggn.publish({ type: 'increment_count', value: 1 });
|
|
188
|
+
siggn.publish({ type: 'custom_event', data: 'test' });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
192
|
+
expect(receivedMessages).toEqual([
|
|
193
|
+
{ type: 'increment_count', value: 1 },
|
|
194
|
+
{ type: 'custom_event', data: 'test' },
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('useSubscribeAll should automatically unsubscribe on component unmount', () => {
|
|
199
|
+
const callback = vi.fn();
|
|
200
|
+
|
|
201
|
+
function TestComponent() {
|
|
202
|
+
useSubscribeAll(siggn, callback);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const { unmount } = render(<TestComponent />);
|
|
207
|
+
|
|
208
|
+
act(() => {
|
|
209
|
+
siggn.publish({ type: 'increment_count', value: 1 });
|
|
210
|
+
});
|
|
211
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
212
|
+
|
|
213
|
+
unmount();
|
|
214
|
+
|
|
215
|
+
act(() => {
|
|
216
|
+
siggn.publish({ type: 'custom_event', data: 'after unmount' });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(callback).toHaveBeenCalledTimes(1); // Should not be called again
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('useSubscribeAll should re-subscribe when dependencies change', () => {
|
|
223
|
+
const callback = vi.fn();
|
|
224
|
+
|
|
225
|
+
function TestComponent({ dep }: { dep: number }) {
|
|
226
|
+
useSubscribeAll(siggn, callback, [dep]);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { rerender } = render(<TestComponent dep={1} />);
|
|
231
|
+
|
|
232
|
+
act(() => {
|
|
233
|
+
siggn.publish({ type: 'increment_count', value: 1 });
|
|
234
|
+
});
|
|
235
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
236
|
+
|
|
237
|
+
rerender(<TestComponent dep={2} />);
|
|
238
|
+
|
|
239
|
+
act(() => {
|
|
240
|
+
siggn.publish({ type: 'increment_count', value: 2 });
|
|
241
|
+
});
|
|
242
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('useSubscribeAll should work with explicit id in options', () => {
|
|
246
|
+
const callback = vi.fn();
|
|
247
|
+
const customId = 'my-global-subscriber';
|
|
248
|
+
|
|
249
|
+
function TestComponent() {
|
|
250
|
+
useSubscribeAll({ instance: siggn, id: customId }, callback);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
render(<TestComponent />);
|
|
255
|
+
|
|
256
|
+
act(() => {
|
|
257
|
+
siggn.publish({ type: 'increment_count', value: 1 });
|
|
258
|
+
});
|
|
259
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
260
|
+
|
|
261
|
+
act(() => {
|
|
262
|
+
siggn.unsubscribeGlobal(customId);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
act(() => {
|
|
266
|
+
siggn.publish({ type: 'increment_count', value: 2 });
|
|
267
|
+
});
|
|
268
|
+
expect(callback).toHaveBeenCalledTimes(1); // Should not be called again
|
|
269
|
+
});
|
|
55
270
|
});
|
package/tsconfig.test.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "../../tsconfig.base.json",
|
|
3
|
-
"include": ["
|
|
3
|
+
"include": ["tests"],
|
|
4
4
|
"references": [
|
|
5
5
|
{
|
|
6
6
|
"path": "tsconfig.src.json"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
],
|
|
9
9
|
"compilerOptions": {
|
|
10
10
|
"tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo",
|
|
11
|
-
"rootDir": "
|
|
11
|
+
"rootDir": "tests",
|
|
12
12
|
"noEmit": true,
|
|
13
13
|
"jsx": "react-jsx",
|
|
14
14
|
"types": ["@testing-library/jest-dom", "vitest/globals"]
|