@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 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
- # @siggn/core
1
+ [![npm version](https://badge.fury.io/js/%40siggn%2Freact.svg)](https://www.npmjs.com/package/%40siggn%2Freact)
2
2
 
3
- A lightweight and type-safe event-driven pub/sub system for TypeScript projects.
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
- - **Type-Safe**: Leverages TypeScript to ensure that message payloads are correct at compile and
8
- runtime.
9
- - **Lightweight**: Zero dependencies and a minimal API surface.
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/core
16
+ npm install @siggn/react
18
17
  ```
19
18
 
20
19
  ```bash
21
- yarn add @siggn/core
20
+ yarn add @siggn/react
22
21
  ```
23
22
 
24
23
  ```bash
25
- pnpm add @siggn/core
24
+ pnpm add @siggn/react
26
25
  ```
27
26
 
28
27
  ## Usage
29
28
 
30
- Here's a basic example of how to use Siggn:
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
- ```typescript
33
- import { Siggn } from '@siggn/core';
31
+ ### 1. Create a Siggn Instance
34
32
 
35
- // 1. Define your message types
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
- // 2. Create a new Siggn instance
41
- const siggn = new Siggn<Message>();
35
+ ```typescript
36
+ // src/siggn.ts
37
+ import { Siggn } from '@siggn/react';
42
38
 
43
- // 3. Subscribe to events
44
- // Use a unique ID for each subscriber to manage subscriptions
45
- const subscriberId = 'analytics-service';
39
+ // Define your message types
40
+ export type Message =
41
+ | { type: 'user_login'; name: string }
42
+ | { type: 'user_logout' };
46
43
 
47
- siggn.subscribe(subscriberId, 'user_created', (msg) => {
48
- console.log(`[Analytics] New user created: ${msg.name} (ID: ${msg.userId})`);
49
- });
44
+ // Create and export the instance
45
+ export const siggn = new Siggn<Message>();
46
+ ```
50
47
 
51
- // 4. Publish events
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
- // 5. Unsubscribe from all events for a given ID
56
- siggn.unsubscribe(subscriberId);
50
+ ### 2. Subscribe to Events in a Component
57
51
 
58
- siggn.publish({ type: 'user_created', userId: '456', name: 'Jane Doe' });
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
- ### Using the `make` helper
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
- The `make` method simplifies managing subscriptions for a specific component or service.
60
+ function Notification() {
61
+ const [notification, setNotification] = useState<string | null>(null);
65
62
 
66
- ```typescript
67
- const userComponent = siggn.make('user-component');
63
+ useSubscribe(siggn, (subscribe) => {
64
+ subscribe('user_login', (msg) => {
65
+ setNotification(`Welcome, ${msg.name}!`);
66
+ });
68
67
 
69
- userComponent.subscribe('user_deleted', (msg) => {
70
- console.log(`[UI] User ${msg.userId} was deleted. Updating view...`);
71
- });
68
+ subscribe('user_logout', () => {
69
+ setNotification('You have been logged out.');
70
+ });
71
+ });
72
72
 
73
- siggn.publish({ type: 'user_deleted', userId: '123' });
74
- // Output: [UI] User 123 was deleted. Updating view...
73
+ if (!notification) {
74
+ return null;
75
+ }
75
76
 
76
- // Unsubscribe from all subscriptions made by 'user-component'
77
- userComponent.unsubscribe();
77
+ return <div className='notification'>{notification}</div>;
78
+ }
78
79
  ```
79
80
 
80
- ### Subscribing to multiple events
81
-
82
- You can use `subscribeMany` to group subscriptions for a single subscriber.
81
+ ### 3. Publish Events
83
82
 
84
- ```typescript
85
- const auditService = siggn.make('audit-service');
83
+ You can publish events from anywhere in your application.
86
84
 
87
- auditService.subscribeMany((subscribe) => {
88
- subscribe('user_created', (msg) => {
89
- console.log(`[Audit] User created: ${msg.name}`);
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
- siggn.publish({ type: 'user_created', userId: '789', name: 'Peter Pan' });
97
- siggn.publish({ type: 'user_deleted', userId: '123' });
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
- // Unsubscribe from all audit-service events
100
- auditService.unsubscribe();
98
+ return <button onClick={handleClick}>{isLoggedIn ? 'Log Out' : 'Log In'}</button>;
99
+ }
101
100
  ```
102
101
 
103
- ### Subscribing to all events
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
- ```typescript
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
- siggn.publish({ type: 'user_created', userId: '789', name: 'Peter Pan' });
116
- // Output: [Logger] Received event of type: user_created
106
+ ```tsx
107
+ // src/components/Logger.tsx
108
+ import { useSubscribeAll } from '@siggn/react';
109
+ import { siggn } from '../siggn';
117
110
 
118
- siggn.publish({ type: 'user_deleted', userId: '123' });
119
- // Output: [Logger] Received event of type: user_deleted
111
+ function Logger() {
112
+ useSubscribeAll(siggn, (msg) => {
113
+ console.log(`[Logger] Event of type ${msg.type} was triggered`);
114
+ });
120
115
 
121
- // Unsubscribe from all logger-service events
122
- logger.unsubscribe();
116
+ return null; // This component does not render anything
117
+ }
123
118
  ```
124
119
 
125
- ### Extending message types with `createChild`
120
+ ### Using `useSiggn`
126
121
 
127
- The `createChild` method allows you to create a new, independent `Siggn` instance that inherits 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
- ```typescript
132
- // Continuing with the previous `Message` type...
133
- const baseSiggn = new Siggn<Message>();
124
+ ```tsx
125
+ import { useSiggn, useSubscribe } from '@siggn/react';
134
126
 
135
- // 1. Define a new set of messages for a specialized module
136
- type AdminMessage = { type: 'admin_login'; adminId: string };
127
+ type LocalMessage = { type: 'local_event' };
137
128
 
138
- // 2. Create a child bus that understands both `Message` and `AdminMessage`
139
- const adminSiggn = baseSiggn.createChild<AdminMessage>();
129
+ function LocalComponent() {
130
+ const localSiggn = useSiggn<LocalMessage>();
140
131
 
141
- // 3. Subscribe to events on the child bus
142
- adminSiggn.subscribe('audit-log', 'user_created', (msg) => {
143
- console.log(`[Admin Audit] User created: ${msg.name}`);
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
- adminSiggn.publish({ type: 'admin_login', adminId: 'xyz' });
155
- // Output: [Admin Auth] Admin logged in: xyz
138
+ const triggerEvent = () => {
139
+ localSiggn.publish({ type: 'local_event' });
140
+ };
156
141
 
157
- // Note: The parent and child buses are independent.
158
- // Publishing on the parent does not affect the child's subscribers.
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
- ### `new Siggn<T>()`
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
- Subscribes a callback to all message types with a unique subscriber ID. The callback will receive
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
- ### `unsubscribe(id)`
152
+ - `T`: A union type of all possible messages.
183
153
 
184
- Removes all subscriptions associated with a specific subscriber ID.
154
+ Returns a `Siggn<T>` instance.
185
155
 
186
- ### `make(id)`
156
+ ### `useSubscribe(options, setup, deps)`
187
157
 
188
- Returns a helper object with `subscribe`, `subscribeMany`, `subscribeAll`, and `unsubscribe` methods
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
- ### `subscribeMany(id, setup)`
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
- A convenience method to subscribe to multiple message types for a single ID.
164
+ ### `useSubscribeAll(options, callback, deps)`
195
165
 
196
- ### `createChild<C>()`
166
+ Subscribes to all messages and automatically unsubscribes when the component unmounts.
197
167
 
198
- Creates a new, independent `Siggn` instance whose message types are a union of the parent's types
199
- and the new child-specific types `C`.
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.1",
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.5"
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
- type SubscriptionOptions<T extends Msg> =
5
- | Siggn<T>
6
- | {
7
- instance: Siggn<T>;
8
- id?: string;
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
- export function useSiggn<T extends Msg>(): Siggn<T> {
37
- const siggn = useRef<Siggn<T>>(new Siggn<T>());
38
- return siggn.current;
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
@@ -0,0 +1,8 @@
1
+ import type { Msg, Siggn } from '@siggn/core';
2
+
3
+ export type SubscriptionOptions<T extends Msg> =
4
+ | Siggn<T>
5
+ | {
6
+ instance: Siggn<T>;
7
+ id?: string;
8
+ };
@@ -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
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "extends": "../../tsconfig.base.json",
3
- "include": ["test"],
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": "test",
11
+ "rootDir": "tests",
12
12
  "noEmit": true,
13
13
  "jsx": "react-jsx",
14
14
  "types": ["@testing-library/jest-dom", "vitest/globals"]