@kuindji/reactive 1.0.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/README.md +985 -0
- package/dist/action.d.ts +49 -0
- package/dist/action.js +77 -0
- package/dist/actionBus.d.ts +43 -0
- package/dist/actionBus.js +81 -0
- package/dist/actionMap.d.ts +23 -0
- package/dist/actionMap.js +26 -0
- package/dist/event.d.ts +143 -0
- package/dist/event.js +538 -0
- package/dist/eventBus.d.ts +115 -0
- package/dist/eventBus.js +508 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/lib/asyncCall.d.ts +1 -0
- package/dist/lib/asyncCall.js +17 -0
- package/dist/lib/listenerSorter.d.ts +5 -0
- package/dist/lib/listenerSorter.js +13 -0
- package/dist/lib/tagsIntersect.d.ts +1 -0
- package/dist/lib/tagsIntersect.js +11 -0
- package/dist/lib/types.d.ts +49 -0
- package/dist/lib/types.js +37 -0
- package/dist/react/ErrorBoundary.d.ts +10 -0
- package/dist/react/ErrorBoundary.js +25 -0
- package/dist/react/useAction.d.ts +21 -0
- package/dist/react/useAction.js +66 -0
- package/dist/react/useActionBus.d.ts +38 -0
- package/dist/react/useActionBus.js +44 -0
- package/dist/react/useActionMap.d.ts +23 -0
- package/dist/react/useActionMap.js +25 -0
- package/dist/react/useEvent.d.ts +47 -0
- package/dist/react/useEvent.js +66 -0
- package/dist/react/useEventBus.d.ts +117 -0
- package/dist/react/useEventBus.js +66 -0
- package/dist/react/useListenToAction.d.ts +3 -0
- package/dist/react/useListenToAction.js +38 -0
- package/dist/react/useListenToEvent.d.ts +4 -0
- package/dist/react/useListenToEvent.js +34 -0
- package/dist/react/useListenToEventBus.d.ts +5 -0
- package/dist/react/useListenToEventBus.js +37 -0
- package/dist/react/useStore.d.ts +51 -0
- package/dist/react/useStore.js +30 -0
- package/dist/react/useStoreState.d.ts +3 -0
- package/dist/react/useStoreState.js +32 -0
- package/dist/react.d.ts +10 -0
- package/dist/react.js +26 -0
- package/dist/store.d.ts +54 -0
- package/dist/store.js +193 -0
- package/package.json +70 -0
- package/src/index.ts +5 -0
package/README.md
ADDED
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
# @kuindji/reactive
|
|
2
|
+
|
|
3
|
+
A JavaScript/TypeScript utility library for building reactive applications with events, actions, stores, and React hooks.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/js/%40kuindji%2Freactive)
|
|
6
|
+
[](https://opensource.org/licenses/ISC)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Event System**: Event emitter with subscriber/dispatcher and collector modes
|
|
12
|
+
- **Action System**: Async action handling with error management and response tracking
|
|
13
|
+
- **Store System**: Reactive state management with change tracking and validation
|
|
14
|
+
- **EventBus**: Centralized event management for complex applications
|
|
15
|
+
- **ActionBus & ActionMap**: Organized action management with error handling
|
|
16
|
+
- **React Integration**: Full React hooks support with error boundaries
|
|
17
|
+
- **TypeScript**: First-class TypeScript support with full type safety
|
|
18
|
+
- **Async Support**: Built-in async/await support for all operations
|
|
19
|
+
- **Error Handling**: Comprehensive error handling and recovery mechanisms
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @kuindji/reactive
|
|
25
|
+
# or
|
|
26
|
+
yarn add @kuindji/reactive
|
|
27
|
+
# or
|
|
28
|
+
bun add @kuindji/reactive
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Table of Contents
|
|
32
|
+
|
|
33
|
+
- [Event](#event)
|
|
34
|
+
- [EventBus](#eventbus)
|
|
35
|
+
- [Action](#action)
|
|
36
|
+
- [ActionMap](#actionmap)
|
|
37
|
+
- [ActionBus](#actionbus)
|
|
38
|
+
- [Store](#store)
|
|
39
|
+
- [React Hooks](#react-hooks)
|
|
40
|
+
- [ErrorBoundary](#errorboundary)
|
|
41
|
+
- [Examples](#examples)
|
|
42
|
+
|
|
43
|
+
## Event
|
|
44
|
+
|
|
45
|
+
Event emitter with three distinct modes:
|
|
46
|
+
|
|
47
|
+
- **Subscriber/Dispatcher Mode**: Traditional event emitter pattern where listeners are notified of events
|
|
48
|
+
- **Collector Mode**: Trigger collects data from listeners in various ways (first, last, all, merge, etc.)
|
|
49
|
+
- **Pipe Mode**: Data flows through listeners in a pipeline, each transforming the data
|
|
50
|
+
|
|
51
|
+
### Basic Usage
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { createEvent } from "@kuindji/reactive";
|
|
55
|
+
|
|
56
|
+
// Create a typed event
|
|
57
|
+
// When creating event, provide a listener signature generic to make full event api typed.
|
|
58
|
+
const userLoginEvent = createEvent<(userId: string, timestamp: Date) => void>();
|
|
59
|
+
|
|
60
|
+
// Add listeners
|
|
61
|
+
userLoginEvent.addListener((userId, timestamp) => {
|
|
62
|
+
console.log(`User ${userId} logged in at ${timestamp}`);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Trigger the event
|
|
66
|
+
userLoginEvent.trigger("user123", new Date());
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Event Options
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// all settings are optional
|
|
73
|
+
const event = createEvent({
|
|
74
|
+
async: boolean, // Call listeners asynchronously; default false
|
|
75
|
+
limit: number, // Event can be triggered 10 times; default 0 (unlimited)
|
|
76
|
+
autoTrigger: boolean, // Auto-trigger new listeners with last args; default false
|
|
77
|
+
maxListeners: number, // Maximum number of listeners; default: 1000
|
|
78
|
+
// default: undefined
|
|
79
|
+
filter: (args: TriggerArgs[], listener: ListenerOptions): boolean => {
|
|
80
|
+
// Custom filter logic
|
|
81
|
+
// args: arguments passed to trigger()
|
|
82
|
+
// listener: an object with listener options and handler itself
|
|
83
|
+
return true;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Listener Options
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// all settings are optional
|
|
92
|
+
event.addListener(handler, {
|
|
93
|
+
limit: number, // Call this listener 5 times; default 0 (unlimited)
|
|
94
|
+
first: boolean, // Add to beginning of listener list; default false
|
|
95
|
+
alwaysFirst: boolean, // Always call before other listeners; default false
|
|
96
|
+
alwaysLast: boolean, // Always call after other listeners; default false
|
|
97
|
+
start: number, // Start calling after 3rd trigger; default 0
|
|
98
|
+
context: object, // Listener context (this); default undefined
|
|
99
|
+
tags: string[], // Listener tags for filtering; default undefined
|
|
100
|
+
async: booleantrue, // Call this listener asynchronously; default false
|
|
101
|
+
extraData: object, // Custom data will be passed to filter()
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Collector
|
|
106
|
+
|
|
107
|
+
Collector allows you to gather data from listeners.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
type ApplicationData = {
|
|
111
|
+
user: {
|
|
112
|
+
username?: string;
|
|
113
|
+
role?: string;
|
|
114
|
+
loggedIn: boolean;
|
|
115
|
+
};
|
|
116
|
+
notifications: {
|
|
117
|
+
type: string;
|
|
118
|
+
message: string;
|
|
119
|
+
}[];
|
|
120
|
+
}
|
|
121
|
+
const event = createEvent(<() => Partial<ApplicationData>>);
|
|
122
|
+
event.addListener(() => {
|
|
123
|
+
return {
|
|
124
|
+
user: {
|
|
125
|
+
username: "john",
|
|
126
|
+
role: "admin",
|
|
127
|
+
loggedIn: true
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
event.addListener(() => {
|
|
132
|
+
return {
|
|
133
|
+
notifications: [
|
|
134
|
+
{
|
|
135
|
+
type: "chat",
|
|
136
|
+
message: "You've got a new message!"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
const applicationData = event.merge();
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Pipe
|
|
147
|
+
|
|
148
|
+
Data flows through listeners in a pipeline, each transforming the data
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const event = createEvent((value: number) => number);
|
|
152
|
+
event.addListener(value => value + value);
|
|
153
|
+
event.addListener(value => value * value);
|
|
154
|
+
const value = event.pipe(1); // value = 4
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Event API
|
|
158
|
+
|
|
159
|
+
#### Core Methods
|
|
160
|
+
|
|
161
|
+
- `addListener(listener, options?)` - Add event listener
|
|
162
|
+
- **Aliases**: `on()`, `listen()`, `subscribe()`
|
|
163
|
+
- `removeListener(listener, context?, tag?)` - Remove specific listener
|
|
164
|
+
- **Aliases**: `un()`, `off()`, `remove()`, `unsubscribe()`
|
|
165
|
+
- `hasListener(listener?, context?, tag?)` - Check if listener exists
|
|
166
|
+
- **Aliases**: `has()`
|
|
167
|
+
- `removeAllListeners(tag?)` - Remove all listeners (optionally by tag)
|
|
168
|
+
- `trigger(...args)` - Trigger the event
|
|
169
|
+
- **Aliases**: `emit()`, `dispatch()`
|
|
170
|
+
|
|
171
|
+
#### Collector Methods
|
|
172
|
+
|
|
173
|
+
- `first(...args)` - Get first listener's result
|
|
174
|
+
- `last(...args)` - Get last listener's result
|
|
175
|
+
- `all(...args)` - Get all listener results
|
|
176
|
+
- `merge(...args)` - Merge all results (for arrays/objects)
|
|
177
|
+
- `concat(...args)` - Concatenate all results
|
|
178
|
+
- `firstNonEmpty(...args)` - Get first non-empty result
|
|
179
|
+
- `untilTrue(...args)` - Stop when listener returns true
|
|
180
|
+
- `untilFalse(...args)` - Stop when listener returns false
|
|
181
|
+
- `pipe(...args)` - Pipe data through listeners
|
|
182
|
+
|
|
183
|
+
#### Async Versions
|
|
184
|
+
|
|
185
|
+
- `resolveFirst(...args)` - Async version of first()
|
|
186
|
+
- `resolveLast(...args)` - Async version of last()
|
|
187
|
+
- `resolveAll(...args)` - Async version of all()
|
|
188
|
+
- `resolveMerge(...args)` - Async version of merge()
|
|
189
|
+
- `resolveConcat(...args)` - Async version of concat()
|
|
190
|
+
- `resolveFirstNonEmpty(...args)` - Async version of firstNonEmpty()
|
|
191
|
+
- `resolvePipe(...args)` - Async version of pipe()
|
|
192
|
+
|
|
193
|
+
#### Utility Methods
|
|
194
|
+
|
|
195
|
+
- `promise(options?: ListenerOptions)` - Get a promise that resolves on next trigger
|
|
196
|
+
- `suspend(withQueue?: boolean)` - Suspend event triggering; When `withQueue=true`, all trigger calls will be queued and replayed after resume()
|
|
197
|
+
- `resume()` - Resume event triggering
|
|
198
|
+
- `reset()` - Reset event state
|
|
199
|
+
- `withTags(tags: string[], callback: () => CallbackResponse) => CallbackResponse` - Execute callback with specific tags
|
|
200
|
+
|
|
201
|
+
## EventBus
|
|
202
|
+
|
|
203
|
+
### Description
|
|
204
|
+
|
|
205
|
+
EventBus provides centralized event management for applications. It allows you to define multiple named events and manage them together with features like event source integration, proxying, and interception.
|
|
206
|
+
|
|
207
|
+
### Basic Usage
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { createEventBus } from "@kuindji/reactive";
|
|
211
|
+
|
|
212
|
+
// Define event signatures
|
|
213
|
+
type AppEvents = {
|
|
214
|
+
userLogin: (userId: string) => void;
|
|
215
|
+
userLogout: (userId: string) => void;
|
|
216
|
+
dataUpdate: (data: any) => void;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Create event bus
|
|
220
|
+
const eventBus = createEventBus<AppEvents>();
|
|
221
|
+
|
|
222
|
+
// Add listeners
|
|
223
|
+
eventBus.on("userLogin", (userId) => {
|
|
224
|
+
console.log(`User ${userId} logged in`);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Trigger events
|
|
228
|
+
eventBus.trigger("userLogin", "user123");
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Relay
|
|
232
|
+
|
|
233
|
+
Relay allows you to forward events from one EventBus to another EventBus.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { createEventBus, ProxyType } from "@kuindji/reactive";
|
|
237
|
+
// Create event buses
|
|
238
|
+
const mainBus = createEventBus<{
|
|
239
|
+
userLogin: (userId: string) => void;
|
|
240
|
+
userLogout: (userId: string) => void;
|
|
241
|
+
dataUpdate: (data: any) => void;
|
|
242
|
+
}>();
|
|
243
|
+
|
|
244
|
+
const externalBus = createEventBus<{
|
|
245
|
+
login: (userId: string) => void;
|
|
246
|
+
logout: (userId: string) => void;
|
|
247
|
+
update: (data: any) => void;
|
|
248
|
+
}>();
|
|
249
|
+
|
|
250
|
+
// Relay events from external bus to main bus
|
|
251
|
+
mainBus.relay({
|
|
252
|
+
eventSource: externalBus,
|
|
253
|
+
remoteEventName: "login",
|
|
254
|
+
localEventName: "userLogin",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
mainBus.relay({
|
|
258
|
+
eventSource: externalBus,
|
|
259
|
+
remoteEventName: "logout",
|
|
260
|
+
localEventName: "userLogout",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
mainBus.relay({
|
|
264
|
+
eventSource: externalBus,
|
|
265
|
+
remoteEventName: "update",
|
|
266
|
+
localEventName: "dataUpdate",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Listen to events on main bus
|
|
270
|
+
mainBus.on("userLogin", (userId) => {
|
|
271
|
+
console.log(`User ${userId} logged in via relay`);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Trigger on external bus - will be relayed to main bus
|
|
275
|
+
externalBus.trigger("login", "user123");
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### Relay with Prefix
|
|
279
|
+
|
|
280
|
+
You can use prefixes to organize relayed events:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// Relay all events with a prefix
|
|
284
|
+
mainBus.relay({
|
|
285
|
+
eventSource: externalBus,
|
|
286
|
+
remoteEventName: "*", // Relay all events
|
|
287
|
+
localEventNamePrefix: "external-",
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Now external events will be available as:
|
|
291
|
+
// external-login, external-logout, external-update
|
|
292
|
+
mainBus.on("external-login", (userId) => {
|
|
293
|
+
console.log(`External login: ${userId}`);
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### Relay with Different Proxy Types
|
|
298
|
+
|
|
299
|
+
You can control how relayed events handle return values:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// Relay with pipe proxy type - data flows through listeners
|
|
303
|
+
mainBus.relay({
|
|
304
|
+
eventSource: externalBus,
|
|
305
|
+
remoteEventName: "processData",
|
|
306
|
+
localEventName: "transformData",
|
|
307
|
+
proxyType: ProxyType.PIPE,
|
|
308
|
+
});
|
|
309
|
+
// now when you call remote "processData" event
|
|
310
|
+
// it will passed through mainBus's "transformData" pipeline and returned to externalBus.
|
|
311
|
+
const transformedData = externalBus.first("processData", { some: data });
|
|
312
|
+
|
|
313
|
+
// Relay with merge proxy type - merge results from all listeners
|
|
314
|
+
mainBus.relay({
|
|
315
|
+
eventSource: externalBus,
|
|
316
|
+
remoteEventName: "collectData",
|
|
317
|
+
localEventName: "aggregateData",
|
|
318
|
+
proxyType: ProxyType.MERGE,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Relay with async resolve proxy type
|
|
322
|
+
mainBus.relay({
|
|
323
|
+
eventSource: externalBus,
|
|
324
|
+
remoteEventName: "asyncOperation",
|
|
325
|
+
localEventName: "handleAsync",
|
|
326
|
+
proxyType: ProxyType.RESOLVE_ALL,
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### Unrelay
|
|
331
|
+
|
|
332
|
+
Stop relaying events:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// Stop relaying specific event
|
|
336
|
+
mainBus.unrelay({
|
|
337
|
+
eventSource: externalBus,
|
|
338
|
+
remoteEventName: "login",
|
|
339
|
+
localEventName: "userLogin",
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Stop relaying all events
|
|
343
|
+
mainBus.unrelay({
|
|
344
|
+
eventSource: externalBus,
|
|
345
|
+
remoteEventName: "*",
|
|
346
|
+
localEventNamePrefix: "external-",
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Event Source
|
|
351
|
+
|
|
352
|
+
Event sources allow you to integrate with external event systems that follow the EventSource interface. This is useful for WebSocket connections, Node.js EventEmitter, or custom event systems.
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import { createEventBus } from "@kuindji/reactive";
|
|
356
|
+
import { EventEmitter } from "events";
|
|
357
|
+
|
|
358
|
+
// Create a Node.js EventEmitter as an event source
|
|
359
|
+
const nodeEmitter = new EventEmitter();
|
|
360
|
+
|
|
361
|
+
// Define the event source interface
|
|
362
|
+
const eventSource = {
|
|
363
|
+
name: "node-emitter",
|
|
364
|
+
on: (name: string, fn: (...args: any[]) => void) => {
|
|
365
|
+
nodeEmitter.on(name, fn);
|
|
366
|
+
},
|
|
367
|
+
un: (name: string, fn: (...args: any[]) => void) => {
|
|
368
|
+
nodeEmitter.off(name, fn);
|
|
369
|
+
},
|
|
370
|
+
accepts: (name: string) => true, // Accept all events
|
|
371
|
+
proxyType: ProxyType.TRIGGER,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Create event bus
|
|
375
|
+
const eventBus = createEventBus<{
|
|
376
|
+
userAction: (action: string, userId: string) => void;
|
|
377
|
+
systemEvent: (event: string, data: any) => void;
|
|
378
|
+
}>();
|
|
379
|
+
|
|
380
|
+
// Add event source
|
|
381
|
+
eventBus.addEventSource(eventSource);
|
|
382
|
+
|
|
383
|
+
// Listen to events
|
|
384
|
+
eventBus.on("userAction", (action, userId) => {
|
|
385
|
+
console.log(`User ${userId} performed action: ${action}`);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Emit on the external source - will be relayed to event bus
|
|
389
|
+
nodeEmitter.emit("userAction", "login", "user123");
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
#### WebSocket Event Source
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// Create WebSocket event source
|
|
396
|
+
const createWebSocketEventSource = (ws: WebSocket) => ({
|
|
397
|
+
name: "websocket",
|
|
398
|
+
on: (name: string, fn: (...args: any[]) => void) => {
|
|
399
|
+
ws.addEventListener("message", (event) => {
|
|
400
|
+
const data = JSON.parse(event.data);
|
|
401
|
+
if (data.type === name) {
|
|
402
|
+
fn(data.payload);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
},
|
|
406
|
+
un: (name: string, fn: (...args: any[]) => void) => {
|
|
407
|
+
ws.removeEventListener("message", fn);
|
|
408
|
+
},
|
|
409
|
+
accepts: (name: string) => true,
|
|
410
|
+
proxyType: ProxyType.TRIGGER,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Usage
|
|
414
|
+
const ws = new WebSocket("ws://localhost:8080");
|
|
415
|
+
const wsEventSource = createWebSocketEventSource(ws);
|
|
416
|
+
|
|
417
|
+
const eventBus = createEventBus<{
|
|
418
|
+
chatMessage: (message: string, userId: string) => void;
|
|
419
|
+
userJoined: (userId: string) => void;
|
|
420
|
+
userLeft: (userId: string) => void;
|
|
421
|
+
}>();
|
|
422
|
+
|
|
423
|
+
eventBus.addEventSource(wsEventSource);
|
|
424
|
+
|
|
425
|
+
// Listen to WebSocket events
|
|
426
|
+
eventBus.on("chatMessage", (message, userId) => {
|
|
427
|
+
console.log(`${userId}: ${message}`);
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
#### Custom Event Source with Filtering
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
// Create custom event source with filtering
|
|
435
|
+
const createCustomEventSource = () => {
|
|
436
|
+
const listeners = new Map<string, Set<(...args: any[]) => void>>();
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
name: "custom-source",
|
|
440
|
+
on: (name: string, fn: (...args: any[]) => void) => {
|
|
441
|
+
if (!listeners.has(name)) {
|
|
442
|
+
listeners.set(name, new Set());
|
|
443
|
+
}
|
|
444
|
+
listeners.get(name)!.add(fn);
|
|
445
|
+
},
|
|
446
|
+
un: (name: string, fn: (...args: any[]) => void) => {
|
|
447
|
+
listeners.get(name)?.delete(fn);
|
|
448
|
+
},
|
|
449
|
+
accepts: (name: string) => name.startsWith("app-"), // Only accept app-* events
|
|
450
|
+
proxyType: ProxyType.TRIGGER,
|
|
451
|
+
|
|
452
|
+
// Custom method to trigger events
|
|
453
|
+
trigger: (name: string, ...args: any[]) => {
|
|
454
|
+
listeners.get(name)?.forEach(fn => fn(...args));
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const customSource = createCustomEventSource();
|
|
460
|
+
const eventBus = createEventBus<{
|
|
461
|
+
appStart: () => void;
|
|
462
|
+
appStop: () => void;
|
|
463
|
+
}>();
|
|
464
|
+
|
|
465
|
+
eventBus.addEventSource(customSource);
|
|
466
|
+
|
|
467
|
+
// Listen to custom events
|
|
468
|
+
eventBus.on("appStart", () => {
|
|
469
|
+
console.log("Application started");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Trigger on custom source
|
|
473
|
+
customSource.trigger("appStart");
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### EventBus API
|
|
477
|
+
|
|
478
|
+
#### Core Methods
|
|
479
|
+
|
|
480
|
+
- `addListener(name, handler, options?)` - Add listener to specific event
|
|
481
|
+
- **Aliases**: `on()`, `listen()`, `subscribe()`
|
|
482
|
+
- `once(name, handler, options?)` - Add one-time listener
|
|
483
|
+
- `removeListener(name, handler, context?, tag?)` - Remove listener
|
|
484
|
+
- **Aliases**: `un()`, `off()`, `remove()`, `unsubscribe()`
|
|
485
|
+
- `trigger(name, ...args)` - Trigger specific event
|
|
486
|
+
- **Aliases**: `emit()`, `dispatch()`
|
|
487
|
+
- `get(name)` - Get event instance by name
|
|
488
|
+
- `add(name, options?)` - Add new event to bus
|
|
489
|
+
|
|
490
|
+
#### Collector Methods
|
|
491
|
+
|
|
492
|
+
- `first(name, ...args)` - Get first listener result
|
|
493
|
+
- `last(name, ...args)` - Get last listener result
|
|
494
|
+
- `all(name, ...args)` - Get all listener results
|
|
495
|
+
- `merge(name, ...args)` - Merge all results
|
|
496
|
+
- `concat(name, ...args)` - Concatenate all results
|
|
497
|
+
- `firstNonEmpty(name, ...args)` - Get first non-empty result
|
|
498
|
+
- `untilTrue(name, ...args)` - Stop when listener returns true
|
|
499
|
+
- `untilFalse(name, ...args)` - Stop when listener returns false
|
|
500
|
+
- `pipe(name, ...args)` - Pipe data through listeners
|
|
501
|
+
|
|
502
|
+
#### Async Versions
|
|
503
|
+
|
|
504
|
+
- `resolveFirst(name, ...args)` - Async version of first()
|
|
505
|
+
- `resolveLast(name, ...args)` - Async version of last()
|
|
506
|
+
- `resolveAll(name, ...args)` - Async version of all()
|
|
507
|
+
- `resolveMerge(name, ...args)` - Async version of merge()
|
|
508
|
+
- `resolveConcat(name, ...args)` - Async version of concat()
|
|
509
|
+
- `resolveFirstNonEmpty(name, ...args)` - Async version of firstNonEmpty()
|
|
510
|
+
- `resolvePipe(name, ...args)` - Async version of pipe()
|
|
511
|
+
|
|
512
|
+
#### Advanced Features
|
|
513
|
+
|
|
514
|
+
- `intercept(fn)` - Intercept all event triggers
|
|
515
|
+
- `stopIntercepting()` - Stop interception
|
|
516
|
+
- `relay(options)` - Relay events from external sources
|
|
517
|
+
- `unrelay(options)` - Stop relaying events
|
|
518
|
+
- `addEventSource(source)` - Add external event source
|
|
519
|
+
- `removeEventSource(source)` - Remove event source
|
|
520
|
+
- `suspendAll(withQueue?)` - Suspend all events
|
|
521
|
+
- `resumeAll()` - Resume all events
|
|
522
|
+
- `reset()` - Reset all events
|
|
523
|
+
- `withTags(tags, callback)` - Execute callback with specific tags
|
|
524
|
+
|
|
525
|
+
## Action
|
|
526
|
+
|
|
527
|
+
### Description
|
|
528
|
+
|
|
529
|
+
Actions are async operations with built-in error handling and response tracking. They provide a structured way to handle async operations and their results.
|
|
530
|
+
|
|
531
|
+
### Basic Usage
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
import { createAction } from "@kuindji/reactive";
|
|
535
|
+
|
|
536
|
+
// Define an async action
|
|
537
|
+
const fetchUserAction = createAction(async (userId: string) => {
|
|
538
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
539
|
+
if (!response.ok) {
|
|
540
|
+
throw new Error("User not found");
|
|
541
|
+
}
|
|
542
|
+
return response.json();
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// Add listeners for success/error
|
|
546
|
+
fetchUserAction.addListener(({ response, error, args }) => {
|
|
547
|
+
if (error) {
|
|
548
|
+
console.error("Action failed:", error);
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
console.log("User data:", response);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Invoke the action
|
|
556
|
+
const result = await fetchUserAction.invoke("user123");
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Action API
|
|
560
|
+
|
|
561
|
+
#### Core Methods
|
|
562
|
+
|
|
563
|
+
- `invoke(...args)` - Execute the action
|
|
564
|
+
- `addListener(handler, options?)` - Add response listener
|
|
565
|
+
- **Aliases**: `on()`, `listen()`, `subscribe()`
|
|
566
|
+
- `removeListener(handler, context?, tag?)` - Remove listener
|
|
567
|
+
- **Aliases**: `un()`, `off()`, `remove()`, `unsubscribe()`
|
|
568
|
+
- `hasListener(handler?, context?, tag?)` - Check if listener exists
|
|
569
|
+
- **Aliases**: `has()`
|
|
570
|
+
- `removeAllListeners(tag?)` - Remove all listeners
|
|
571
|
+
|
|
572
|
+
#### Error Handling
|
|
573
|
+
|
|
574
|
+
- `addErrorListener(handler, context?)` - Add error listener
|
|
575
|
+
- `removeErrorListener(handler, context?)` - Remove error listener
|
|
576
|
+
- `hasErrorListeners()` - Check if error listeners exist
|
|
577
|
+
- `removeAllErrorListeners(tag?)` - Remove all error listeners
|
|
578
|
+
|
|
579
|
+
#### Utility Methods
|
|
580
|
+
|
|
581
|
+
- `promise(options?)` - Get promise for next invocation
|
|
582
|
+
- `suspend(withQueue?)` - Suspend action execution
|
|
583
|
+
- `resume()` - Resume action execution
|
|
584
|
+
- `reset()` - Reset action state
|
|
585
|
+
|
|
586
|
+
## ActionMap
|
|
587
|
+
|
|
588
|
+
### Description
|
|
589
|
+
|
|
590
|
+
ActionMap provides a way to organize multiple actions with centralized error handling. It's useful for managing related actions in a structured way.
|
|
591
|
+
|
|
592
|
+
### Basic Usage
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
import { createActionMap } from "@kuindji/reactive";
|
|
596
|
+
|
|
597
|
+
// Define actions
|
|
598
|
+
const actions = {
|
|
599
|
+
fetchUser: async (userId: string) => {
|
|
600
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
601
|
+
return response.json();
|
|
602
|
+
},
|
|
603
|
+
updateUser: async (userId: string, data: any) => {
|
|
604
|
+
const response = await fetch(`/api/users/${userId}`, {
|
|
605
|
+
method: "PUT",
|
|
606
|
+
body: JSON.stringify(data),
|
|
607
|
+
});
|
|
608
|
+
return response.json();
|
|
609
|
+
},
|
|
610
|
+
deleteUser: async (userId: string) => {
|
|
611
|
+
await fetch(`/api/users/${userId}`, { method: "DELETE" });
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// type ErrorResponse = {
|
|
616
|
+
// args: TriggerArgs[],
|
|
617
|
+
// error: Error,
|
|
618
|
+
// name?: ActionName,
|
|
619
|
+
// type?: "action"
|
|
620
|
+
// }
|
|
621
|
+
|
|
622
|
+
// Create action map with error handling
|
|
623
|
+
const actionMap = createActionMap(actions, (errorResponse: ErrorResponse) => {
|
|
624
|
+
console.error("Action failed:", errorResponse);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Use actions
|
|
628
|
+
const user = await actionMap.fetchUser.invoke("user123");
|
|
629
|
+
await actionMap.updateUser.invoke("user123", { name: "John" });
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### ActionMap API
|
|
633
|
+
|
|
634
|
+
The ActionMap returns an object where each key corresponds to an action with the same API as individual actions created with `createAction()`.
|
|
635
|
+
|
|
636
|
+
## ActionBus
|
|
637
|
+
|
|
638
|
+
### Description
|
|
639
|
+
|
|
640
|
+
ActionBus provides centralized action management similar to EventBus but for actions. It allows you to dynamically add and manage actions with built-in error handling.
|
|
641
|
+
|
|
642
|
+
### Basic Usage
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
import { createActionBus } from "@kuindji/reactive";
|
|
646
|
+
|
|
647
|
+
type Actions = {
|
|
648
|
+
fetchUser: (userId: string) => Promise<UserData>;
|
|
649
|
+
updateUser: (userId: string, data: UserData) => Promise<UserData>;
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// Create action bus
|
|
653
|
+
const actionBus = createActionBus<Actions>();
|
|
654
|
+
|
|
655
|
+
// Add actions
|
|
656
|
+
actionBus.add("fetchUser", async (userId) => {
|
|
657
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
658
|
+
return response.json();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
actionBus.add("updateUser", async (userId, data) => {
|
|
662
|
+
const response = await fetch(`/api/users/${userId}`, {
|
|
663
|
+
method: "PUT",
|
|
664
|
+
body: JSON.stringify(data),
|
|
665
|
+
});
|
|
666
|
+
return response.json();
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Add listeners
|
|
670
|
+
actionBus.on("fetchUser", ({ response, error }) => {
|
|
671
|
+
if (error) {
|
|
672
|
+
console.error("Failed to fetch user:", error);
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
console.log("User fetched:", response);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// Invoke actions
|
|
680
|
+
const user = await actionBus.invoke("fetchUser", "user123");
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### ActionBus API
|
|
684
|
+
|
|
685
|
+
#### Core Methods
|
|
686
|
+
|
|
687
|
+
- `add(name, action)` - Add action to bus
|
|
688
|
+
- `get(name)` - Get action by name
|
|
689
|
+
- `invoke(name, ...args)` - Invoke action by name
|
|
690
|
+
- `addListener(name, handler, options?)` - Add listener to action
|
|
691
|
+
- **Aliases**: `on()`, `listen()`, `subscribe()`
|
|
692
|
+
- `once(name, handler, options?)` - Add one-time listener
|
|
693
|
+
- `removeListener(name, handler, context?, tag?)` - Remove listener
|
|
694
|
+
- **Aliases**: `un()`, `off()`, `remove()`, `unsubscribe()`
|
|
695
|
+
- `has(name)` - Check if action exists
|
|
696
|
+
- `remove(name)` - Remove action
|
|
697
|
+
|
|
698
|
+
#### Error Handling
|
|
699
|
+
|
|
700
|
+
- `addErrorListener(handler)` - Add global error listener
|
|
701
|
+
- `removeErrorListener(handler)` - Remove global error listener
|
|
702
|
+
- `hasErrorListeners()` - Check if error listeners exist
|
|
703
|
+
|
|
704
|
+
## Store
|
|
705
|
+
|
|
706
|
+
### Description
|
|
707
|
+
|
|
708
|
+
Store provides reactive state management with change tracking, validation, and event-driven updates. It's designed for managing application state with full TypeScript support.
|
|
709
|
+
|
|
710
|
+
### Basic Usage
|
|
711
|
+
|
|
712
|
+
```typescript
|
|
713
|
+
import { createStore } from "@kuindji/reactive";
|
|
714
|
+
|
|
715
|
+
// Define store schema
|
|
716
|
+
type UserStore = {
|
|
717
|
+
id: string;
|
|
718
|
+
name: string;
|
|
719
|
+
email: string;
|
|
720
|
+
isLoggedIn: boolean;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// Create store with initial data
|
|
724
|
+
const userStore = createStore<UserStore>({
|
|
725
|
+
id: "",
|
|
726
|
+
name: "",
|
|
727
|
+
email: "",
|
|
728
|
+
isLoggedIn: false,
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// Listen to changes
|
|
732
|
+
userStore.onChange("name", (newName, oldName) => {
|
|
733
|
+
console.log(`Name changed from ${oldName} to ${newName}`);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Update store
|
|
737
|
+
userStore.set("name", "John Doe");
|
|
738
|
+
userStore.set("isLoggedIn", true);
|
|
739
|
+
|
|
740
|
+
// Get values
|
|
741
|
+
const name = userStore.get("name");
|
|
742
|
+
const userData = userStore.get([ "name", "email" ]); // { name: string, email: string }
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### Store API
|
|
746
|
+
|
|
747
|
+
#### Core Methods
|
|
748
|
+
|
|
749
|
+
- `set(key, value)` - Set single property
|
|
750
|
+
- `set(data)` - Set multiple properties
|
|
751
|
+
- `asyncSet(key, value)` - Async set single property
|
|
752
|
+
- `asyncSet(data)` - Async set multiple properties
|
|
753
|
+
- `get(key)` - Get single property
|
|
754
|
+
- `get(keys)` - Get multiple properties
|
|
755
|
+
- `isEmpty()` - Check if store is empty
|
|
756
|
+
- `getData()` - Get all store data
|
|
757
|
+
- `reset()` - Reset store to initial state
|
|
758
|
+
|
|
759
|
+
#### Event Methods
|
|
760
|
+
|
|
761
|
+
- `onChange(key, handler)` - Listen to property changes
|
|
762
|
+
- `pipe(key, handler)` - Add data transformation pipeline
|
|
763
|
+
- `control(event, handler)` - Control store events
|
|
764
|
+
|
|
765
|
+
#### Control Events
|
|
766
|
+
|
|
767
|
+
- `beforeChange` - Fired before property changes (can prevent change)
|
|
768
|
+
- `change` - Fired after properties change
|
|
769
|
+
- `reset` - Fired when store is reset
|
|
770
|
+
- `error` - Fired when errors occur
|
|
771
|
+
|
|
772
|
+
#### Utility Methods
|
|
773
|
+
|
|
774
|
+
- `batch(fn)` - Batch multiple changes
|
|
775
|
+
|
|
776
|
+
## React Hooks
|
|
777
|
+
|
|
778
|
+
### Description
|
|
779
|
+
|
|
780
|
+
The library provides comprehensive React hooks for integrating reactive functionality into React components with automatic cleanup and error handling.
|
|
781
|
+
|
|
782
|
+
### Basic Usage
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
import { useListenToEvent } from "@kuindji/reactive/react";
|
|
786
|
+
|
|
787
|
+
const event = createEvent(() => void);
|
|
788
|
+
|
|
789
|
+
function FirstComponent() {
|
|
790
|
+
|
|
791
|
+
// Use in component
|
|
792
|
+
const handleClick = () => {
|
|
793
|
+
event.trigger();
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
return (
|
|
797
|
+
<div>
|
|
798
|
+
<button onClick={handleClick}>Trigger event</button>
|
|
799
|
+
</div>
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function AnotherComponent() {
|
|
804
|
+
const handler = useCallback(
|
|
805
|
+
() => {
|
|
806
|
+
console.log("something happened in first component")
|
|
807
|
+
},
|
|
808
|
+
[]
|
|
809
|
+
);
|
|
810
|
+
useListenToEvent(event, handler);
|
|
811
|
+
}
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### Available Hooks
|
|
815
|
+
|
|
816
|
+
Create and use event
|
|
817
|
+
|
|
818
|
+
```typescript
|
|
819
|
+
const event = useEvent(
|
|
820
|
+
listenerOptions?: ListenerOptions,
|
|
821
|
+
listener?: Function,
|
|
822
|
+
errorListener?: (errorResponse) => void
|
|
823
|
+
);
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
Listen to event
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
useListenToEvent(
|
|
830
|
+
event: Event,
|
|
831
|
+
listener?: Function,
|
|
832
|
+
errorListener?: (errorResponse) => void
|
|
833
|
+
)
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
Create and use event bus
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
type EventBus = {
|
|
840
|
+
eventName: Function
|
|
841
|
+
}
|
|
842
|
+
type EventBusOptions = {
|
|
843
|
+
eventName: EventOptions
|
|
844
|
+
}
|
|
845
|
+
const eventBus = useEventBus<EventBus>(
|
|
846
|
+
options?: EventBusOptions,
|
|
847
|
+
allEventsListener?: Function,
|
|
848
|
+
errorListener?: (errorResponse) => void
|
|
849
|
+
);
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
Listen to bus events
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
useListenToEventBus(
|
|
856
|
+
eventBus: EventBus,
|
|
857
|
+
eventName: string,
|
|
858
|
+
listener: Function,
|
|
859
|
+
listenerOptions?: ListenerOptions,
|
|
860
|
+
errorListener?: (errorResponse) => void
|
|
861
|
+
)
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
Create and use action
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
const action = useAction(
|
|
868
|
+
action: Function,
|
|
869
|
+
listener?: Function,
|
|
870
|
+
errorListener?: (errorResponse) => void
|
|
871
|
+
);
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
Listen to action events
|
|
875
|
+
|
|
876
|
+
```typescript
|
|
877
|
+
useListenToAction(
|
|
878
|
+
action: Action,
|
|
879
|
+
listener?: Function,
|
|
880
|
+
errorListener?: (errorResponse) => void
|
|
881
|
+
)
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
Create and use action map
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
const actionMap = useActionMap(
|
|
888
|
+
actions: {
|
|
889
|
+
actionName: Function
|
|
890
|
+
},
|
|
891
|
+
errorListener?: (errorResponse) => void
|
|
892
|
+
)
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
Create and use action bus
|
|
896
|
+
|
|
897
|
+
```typescript
|
|
898
|
+
type ActionsMap = {
|
|
899
|
+
actionName: FunctionSignature
|
|
900
|
+
}
|
|
901
|
+
const actionBus = useActionBus<ActionsMap>(
|
|
902
|
+
initialActions: Partial<ActionsMap>,
|
|
903
|
+
errorListener?: (errorResponse) => void
|
|
904
|
+
)
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
Create and use data store
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
910
|
+
type PropTypes = {
|
|
911
|
+
propName: number
|
|
912
|
+
}
|
|
913
|
+
const store = useStore<PropTypes>(
|
|
914
|
+
initialData: Partial<PropTypes>,
|
|
915
|
+
config: {
|
|
916
|
+
onChange: {
|
|
917
|
+
[K in keyof PropTypes]: (
|
|
918
|
+
value: PropTypes[K],
|
|
919
|
+
prevValue: PropTypes[K] | undefined
|
|
920
|
+
) => void
|
|
921
|
+
};
|
|
922
|
+
pipes: {
|
|
923
|
+
[K in keyof PropTypes]: (
|
|
924
|
+
value: PropTypes[K]
|
|
925
|
+
) => PropTypes[K]
|
|
926
|
+
};
|
|
927
|
+
control: {
|
|
928
|
+
beforeChange: (name, value) => boolean;
|
|
929
|
+
change: (names) => void;
|
|
930
|
+
error: (errorResponse) => void;
|
|
931
|
+
reset: () => void;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
);
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
Use store value as state
|
|
938
|
+
|
|
939
|
+
```typescript
|
|
940
|
+
const [ value, setValue ] = useStoreStore(
|
|
941
|
+
store: Store,
|
|
942
|
+
key: string
|
|
943
|
+
)
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
## ErrorBoundary
|
|
947
|
+
|
|
948
|
+
### Description
|
|
949
|
+
|
|
950
|
+
ErrorBoundary provides catch-all error listener for actions and events. Without ErrorBoundary (or with empty "listener") and without error listener passed directly to them they will re-throw errors from listeners.
|
|
951
|
+
|
|
952
|
+
### Basic Usage
|
|
953
|
+
|
|
954
|
+
```typescript
|
|
955
|
+
import { ErrorBoundary } from "@kuindji/reactive/react";
|
|
956
|
+
|
|
957
|
+
function App() {
|
|
958
|
+
return (
|
|
959
|
+
<ErrorBoundary
|
|
960
|
+
listener={(errorResponse) => {
|
|
961
|
+
console.error("Reactive error:", errorResponse);
|
|
962
|
+
// Send to error reporting service
|
|
963
|
+
}}>
|
|
964
|
+
<UserComponent />
|
|
965
|
+
</ErrorBoundary>
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
### ErrorBoundary API
|
|
971
|
+
|
|
972
|
+
- `children` - React children to render
|
|
973
|
+
- `listener` - Error listener function (optional)
|
|
974
|
+
|
|
975
|
+
## License
|
|
976
|
+
|
|
977
|
+
ISC License
|
|
978
|
+
|
|
979
|
+
## Contributing
|
|
980
|
+
|
|
981
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
982
|
+
|
|
983
|
+
## Support
|
|
984
|
+
|
|
985
|
+
If you encounter any issues or have questions, please [open an issue](https://github.com/kuindji/reactive/issues) on GitHub.
|