@go-go-scope/adapter-react 2.7.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/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/index.d.mts +289 -0
- package/dist/index.mjs +262 -0
- package/package.json +50 -0
- package/src/index.ts +636 -0
- package/tests/react.test.tsx +383 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thelinuxlich (Alisson Cavalcante Agiani)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @go-go-scope/adapter-react
|
|
2
|
+
|
|
3
|
+
React hooks for go-go-scope. Provides reactive integration with React's hooks API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @go-go-scope/adapter-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { useScope, useTask, useChannel } from '@go-go-scope/adapter-react';
|
|
15
|
+
|
|
16
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
17
|
+
// Create a reactive scope
|
|
18
|
+
const scope = useScope({ name: "UserProfile" });
|
|
19
|
+
|
|
20
|
+
// Execute tasks with reactive state
|
|
21
|
+
const { data, error, isLoading, execute } = useTask(
|
|
22
|
+
async () => {
|
|
23
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
24
|
+
return response.json();
|
|
25
|
+
},
|
|
26
|
+
{ immediate: true }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Use channels for communication
|
|
30
|
+
const channel = useChannel<string>();
|
|
31
|
+
|
|
32
|
+
const sendMessage = async () => {
|
|
33
|
+
await channel.send("Hello!");
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (isLoading) return <div>Loading...</div>;
|
|
37
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div>
|
|
41
|
+
<h1>{data?.name}</h1>
|
|
42
|
+
<p>Latest message: {channel.latest}</p>
|
|
43
|
+
<button onClick={sendMessage}>Send Message</button>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
### `useScope(options?)`
|
|
52
|
+
Creates a reactive scope that auto-disposes on component unmount.
|
|
53
|
+
|
|
54
|
+
### `useTask(factory, options?)`
|
|
55
|
+
Creates a reactive task with `data`, `error`, `isLoading`, `isReady`, and `execute()`.
|
|
56
|
+
|
|
57
|
+
### `useParallel(factories, options?)`
|
|
58
|
+
Executes tasks in parallel with progress tracking.
|
|
59
|
+
|
|
60
|
+
### `useChannel(options?)`
|
|
61
|
+
Creates a reactive channel for Go-style communication.
|
|
62
|
+
|
|
63
|
+
### `useBroadcast()`
|
|
64
|
+
Creates a reactive broadcast channel for pub/sub patterns.
|
|
65
|
+
|
|
66
|
+
### `usePolling(factory, options)`
|
|
67
|
+
Creates a reactive polling mechanism.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { Result, Scope } from 'go-go-scope';
|
|
2
|
+
export { BroadcastChannel, Channel, Result, Scope } from 'go-go-scope';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React hooks for go-go-scope
|
|
6
|
+
*
|
|
7
|
+
* Provides React hooks integration with go-go-scope's structured concurrency.
|
|
8
|
+
* All hooks automatically clean up when components unmount.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { useScope, useTask, useChannel } from "@go-go-scope/adapter-react";
|
|
13
|
+
*
|
|
14
|
+
* function UserProfile({ userId }: { userId: string }) {
|
|
15
|
+
* // Auto-disposing scope
|
|
16
|
+
* const scope = useScope({ name: "UserProfile" });
|
|
17
|
+
*
|
|
18
|
+
* // Reactive task with states
|
|
19
|
+
* const { data, error, isLoading, execute } = useTask(
|
|
20
|
+
* async () => fetchUser(userId),
|
|
21
|
+
* { immediate: true }
|
|
22
|
+
* );
|
|
23
|
+
*
|
|
24
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
25
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
26
|
+
* return <div>Hello, {data?.name}</div>;
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Options for useScope hook
|
|
33
|
+
*/
|
|
34
|
+
interface UseScopeOptions {
|
|
35
|
+
/** Scope name for debugging */
|
|
36
|
+
name?: string;
|
|
37
|
+
/** Timeout in milliseconds */
|
|
38
|
+
timeout?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* React hook that creates a reactive scope.
|
|
42
|
+
* The scope automatically disposes when the component unmounts.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* function MyComponent() {
|
|
47
|
+
* const s = useScope({ name: "MyComponent" });
|
|
48
|
+
*
|
|
49
|
+
* const handleClick = async () => {
|
|
50
|
+
* const [err, result] = await s.task(() => fetchData());
|
|
51
|
+
* // Handle result...
|
|
52
|
+
* };
|
|
53
|
+
*
|
|
54
|
+
* return <button onClick={handleClick}>Fetch</button>;
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
declare function useScope(options?: UseScopeOptions): Scope<Record<string, unknown>>;
|
|
59
|
+
/**
|
|
60
|
+
* Options for useTask hook
|
|
61
|
+
*/
|
|
62
|
+
interface UseTaskOptions<T> {
|
|
63
|
+
/** Task name for debugging */
|
|
64
|
+
name?: string;
|
|
65
|
+
/** Whether to execute immediately */
|
|
66
|
+
immediate?: boolean;
|
|
67
|
+
/** Timeout in milliseconds */
|
|
68
|
+
timeout?: number;
|
|
69
|
+
/** Retry options */
|
|
70
|
+
retry?: {
|
|
71
|
+
maxRetries?: number;
|
|
72
|
+
delay?: number;
|
|
73
|
+
};
|
|
74
|
+
/** Initial data value */
|
|
75
|
+
initialData?: T;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* State returned by useTask hook
|
|
79
|
+
*/
|
|
80
|
+
interface TaskState<T> {
|
|
81
|
+
/** Current data (undefined if not loaded) */
|
|
82
|
+
data: T | undefined;
|
|
83
|
+
/** Error if task failed */
|
|
84
|
+
error: Error | undefined;
|
|
85
|
+
/** Whether task is currently running */
|
|
86
|
+
isLoading: boolean;
|
|
87
|
+
/** Whether task has been executed */
|
|
88
|
+
isReady: boolean;
|
|
89
|
+
/** Execute the task */
|
|
90
|
+
execute: () => Promise<Result<Error, T>>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* React hook for executing tasks with structured concurrency.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```tsx
|
|
97
|
+
* function UserProfile({ userId }: { userId: string }) {
|
|
98
|
+
* const { data, error, isLoading } = useTask(
|
|
99
|
+
* async () => fetchUser(userId),
|
|
100
|
+
* { immediate: true }
|
|
101
|
+
* );
|
|
102
|
+
*
|
|
103
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
104
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
105
|
+
* return <div>{data?.name}</div>;
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
declare function useTask<T>(factory: (signal: AbortSignal) => Promise<T>, options?: UseTaskOptions<T>): TaskState<T>;
|
|
110
|
+
/**
|
|
111
|
+
* Options for useParallel hook
|
|
112
|
+
*/
|
|
113
|
+
interface UseParallelOptions {
|
|
114
|
+
/** Concurrency limit */
|
|
115
|
+
concurrency?: number;
|
|
116
|
+
/** Whether to execute immediately */
|
|
117
|
+
immediate?: boolean;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* State returned by useParallel hook
|
|
121
|
+
*/
|
|
122
|
+
interface ParallelState<T> {
|
|
123
|
+
/** Results from all tasks */
|
|
124
|
+
results: (T | undefined)[];
|
|
125
|
+
/** Errors from failed tasks */
|
|
126
|
+
errors: (Error | undefined)[];
|
|
127
|
+
/** Whether any task is running */
|
|
128
|
+
isLoading: boolean;
|
|
129
|
+
/** Progress percentage (0-100) */
|
|
130
|
+
progress: number;
|
|
131
|
+
/** Execute all tasks */
|
|
132
|
+
execute: () => Promise<Result<Error, T>[]>;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* React hook for executing tasks in parallel.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```tsx
|
|
139
|
+
* function Dashboard() {
|
|
140
|
+
* const factories = [
|
|
141
|
+
* () => fetchUsers(),
|
|
142
|
+
* () => fetchPosts(),
|
|
143
|
+
* () => fetchComments(),
|
|
144
|
+
* ];
|
|
145
|
+
*
|
|
146
|
+
* const { results, isLoading, progress } = useParallel(factories, {
|
|
147
|
+
* concurrency: 2,
|
|
148
|
+
* immediate: true
|
|
149
|
+
* });
|
|
150
|
+
*
|
|
151
|
+
* if (isLoading) return <progress value={progress} max="100" />;
|
|
152
|
+
*
|
|
153
|
+
* const [users, posts, comments] = results;
|
|
154
|
+
* return <DashboardView {...{ users, posts, comments }} />;
|
|
155
|
+
* }
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
declare function useParallel<T>(factories: (() => Promise<T>)[], options?: UseParallelOptions): ParallelState<T>;
|
|
159
|
+
/**
|
|
160
|
+
* Options for useChannel hook
|
|
161
|
+
*/
|
|
162
|
+
interface UseChannelOptions {
|
|
163
|
+
/** Buffer size */
|
|
164
|
+
bufferSize?: number;
|
|
165
|
+
/** Maximum history to keep */
|
|
166
|
+
historySize?: number;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* State returned by useChannel hook
|
|
170
|
+
*/
|
|
171
|
+
interface ChannelState<T> {
|
|
172
|
+
/** Latest received value */
|
|
173
|
+
latest: T | undefined;
|
|
174
|
+
/** History of values */
|
|
175
|
+
history: readonly T[];
|
|
176
|
+
/** Whether channel is closed */
|
|
177
|
+
isClosed: boolean;
|
|
178
|
+
/** Send a value */
|
|
179
|
+
send: (value: T) => Promise<void>;
|
|
180
|
+
/** Close the channel */
|
|
181
|
+
close: () => void;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* React hook for Go-style channel communication.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```tsx
|
|
188
|
+
* function Chat() {
|
|
189
|
+
* const ch = useChannel<string>();
|
|
190
|
+
*
|
|
191
|
+
* const handleSubmit = async (message: string) => {
|
|
192
|
+
* await ch.send(message);
|
|
193
|
+
* };
|
|
194
|
+
*
|
|
195
|
+
* return (
|
|
196
|
+
* <div>
|
|
197
|
+
* <p>Latest: {ch.latest}</p>
|
|
198
|
+
* <ul>
|
|
199
|
+
* {ch.history.map((msg, i) => <li key={i}>{msg}</li>)}
|
|
200
|
+
* </ul>
|
|
201
|
+
* </div>
|
|
202
|
+
* );
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
declare function useChannel<T>(options?: UseChannelOptions): ChannelState<T>;
|
|
207
|
+
/**
|
|
208
|
+
* State returned by useBroadcast hook
|
|
209
|
+
*/
|
|
210
|
+
interface BroadcastState<T> {
|
|
211
|
+
/** Latest broadcasted value */
|
|
212
|
+
latest: T | undefined;
|
|
213
|
+
/** Subscribe to broadcasts */
|
|
214
|
+
subscribe: (callback: (value: T) => void) => {
|
|
215
|
+
unsubscribe: () => void;
|
|
216
|
+
};
|
|
217
|
+
/** Broadcast a value */
|
|
218
|
+
broadcast: (value: T) => void;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* React hook for pub/sub broadcast channels.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```tsx
|
|
225
|
+
* function EventBus() {
|
|
226
|
+
* const bus = useBroadcast<string>();
|
|
227
|
+
*
|
|
228
|
+
* useEffect(() => {
|
|
229
|
+
* const sub = bus.subscribe((msg) => console.log(msg));
|
|
230
|
+
* return () => sub.unsubscribe();
|
|
231
|
+
* }, []);
|
|
232
|
+
*
|
|
233
|
+
* return <button onClick={() => bus.broadcast("Hello!")}>Send</button>;
|
|
234
|
+
* }
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
declare function useBroadcast<T>(): BroadcastState<T>;
|
|
238
|
+
/**
|
|
239
|
+
* Options for usePolling hook
|
|
240
|
+
*/
|
|
241
|
+
interface UsePollingOptions {
|
|
242
|
+
/** Interval in milliseconds */
|
|
243
|
+
interval: number;
|
|
244
|
+
/** Whether to start immediately */
|
|
245
|
+
immediate?: boolean;
|
|
246
|
+
/** Continue when tab is hidden */
|
|
247
|
+
continueOnHidden?: boolean;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* State returned by usePolling hook
|
|
251
|
+
*/
|
|
252
|
+
interface PollingState<T> {
|
|
253
|
+
/** Latest data */
|
|
254
|
+
data: T | undefined;
|
|
255
|
+
/** Error if polling failed */
|
|
256
|
+
error: Error | undefined;
|
|
257
|
+
/** Whether currently polling */
|
|
258
|
+
isPolling: boolean;
|
|
259
|
+
/** Number of polls completed */
|
|
260
|
+
pollCount: number;
|
|
261
|
+
/** Start polling */
|
|
262
|
+
start: () => void;
|
|
263
|
+
/** Stop polling */
|
|
264
|
+
stop: () => void;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* React hook for polling data at intervals.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```tsx
|
|
271
|
+
* function LiveData() {
|
|
272
|
+
* const { data, isPolling, stop } = usePolling(
|
|
273
|
+
* async () => fetchLatestData(),
|
|
274
|
+
* { interval: 5000, immediate: true }
|
|
275
|
+
* );
|
|
276
|
+
*
|
|
277
|
+
* return (
|
|
278
|
+
* <div>
|
|
279
|
+
* <p>{data}</p>
|
|
280
|
+
* {isPolling && <button onClick={stop}>Stop</button>}
|
|
281
|
+
* </div>
|
|
282
|
+
* );
|
|
283
|
+
* }
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
declare function usePolling<T>(factory: () => Promise<T>, options: UsePollingOptions): PollingState<T>;
|
|
287
|
+
|
|
288
|
+
export { useBroadcast, useChannel, useParallel, usePolling, useScope, useTask };
|
|
289
|
+
export type { BroadcastState, ChannelState, ParallelState, PollingState, TaskState, UseChannelOptions, UseParallelOptions, UsePollingOptions, UseScopeOptions, UseTaskOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { scope, BroadcastChannel } from 'go-go-scope';
|
|
2
|
+
import { useRef, useEffect, useState, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
function useScope(options = {}) {
|
|
5
|
+
const scopeRef = useRef(null);
|
|
6
|
+
if (!scopeRef.current) {
|
|
7
|
+
scopeRef.current = scope({
|
|
8
|
+
name: options.name,
|
|
9
|
+
timeout: options.timeout
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
return () => {
|
|
14
|
+
scopeRef.current?.[Symbol.asyncDispose]().catch(() => {
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
}, []);
|
|
18
|
+
return scopeRef.current;
|
|
19
|
+
}
|
|
20
|
+
function useTask(factory, options = {}) {
|
|
21
|
+
const s = useScope({ name: options.name ?? "useTask" });
|
|
22
|
+
const [data, setData] = useState(options.initialData);
|
|
23
|
+
const [error, setError] = useState(void 0);
|
|
24
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
25
|
+
const [isReady, setIsReady] = useState(false);
|
|
26
|
+
const execute = useCallback(async () => {
|
|
27
|
+
setIsLoading(true);
|
|
28
|
+
setError(void 0);
|
|
29
|
+
try {
|
|
30
|
+
const [err, result] = await s.task(
|
|
31
|
+
async ({ signal }) => await factory(signal),
|
|
32
|
+
{
|
|
33
|
+
timeout: options.timeout,
|
|
34
|
+
retry: options.retry
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
if (err) {
|
|
38
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
39
|
+
setData(void 0);
|
|
40
|
+
return [err, void 0];
|
|
41
|
+
}
|
|
42
|
+
setData(result);
|
|
43
|
+
setIsReady(true);
|
|
44
|
+
return [void 0, result];
|
|
45
|
+
} finally {
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
}
|
|
48
|
+
}, [s, factory, options.timeout, options.retry]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (options.immediate) {
|
|
51
|
+
execute();
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
return {
|
|
55
|
+
data,
|
|
56
|
+
error,
|
|
57
|
+
isLoading,
|
|
58
|
+
isReady,
|
|
59
|
+
execute
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function useParallel(factories, options = {}) {
|
|
63
|
+
const s = useScope({ name: "useParallel" });
|
|
64
|
+
const [results, setResults] = useState([]);
|
|
65
|
+
const [errors, setErrors] = useState([]);
|
|
66
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
67
|
+
const [progress, setProgress] = useState(0);
|
|
68
|
+
const execute = useCallback(async () => {
|
|
69
|
+
setIsLoading(true);
|
|
70
|
+
setProgress(0);
|
|
71
|
+
setResults(new Array(factories.length).fill(void 0));
|
|
72
|
+
setErrors(new Array(factories.length).fill(void 0));
|
|
73
|
+
try {
|
|
74
|
+
const taskFactories = factories.map((factory, index) => {
|
|
75
|
+
return async () => {
|
|
76
|
+
const [err, result] = await s.task(async () => await factory());
|
|
77
|
+
if (err) {
|
|
78
|
+
setErrors((prev) => {
|
|
79
|
+
const next = [...prev];
|
|
80
|
+
next[index] = err instanceof Error ? err : new Error(String(err));
|
|
81
|
+
return next;
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
setResults((prev) => {
|
|
85
|
+
const next = [...prev];
|
|
86
|
+
next[index] = result;
|
|
87
|
+
return next;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
setProgress(Math.round((index + 1) / factories.length * 100));
|
|
91
|
+
return [err, result];
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
return await s.parallel(taskFactories, {
|
|
95
|
+
concurrency: options.concurrency
|
|
96
|
+
});
|
|
97
|
+
} finally {
|
|
98
|
+
setIsLoading(false);
|
|
99
|
+
}
|
|
100
|
+
}, [s, factories, options.concurrency]);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (options.immediate) {
|
|
103
|
+
execute();
|
|
104
|
+
}
|
|
105
|
+
}, []);
|
|
106
|
+
return {
|
|
107
|
+
results,
|
|
108
|
+
errors,
|
|
109
|
+
isLoading,
|
|
110
|
+
progress,
|
|
111
|
+
execute
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function useChannel(options = {}) {
|
|
115
|
+
const s = useScope({ name: "useChannel" });
|
|
116
|
+
const chRef = useRef(null);
|
|
117
|
+
if (!chRef.current) {
|
|
118
|
+
chRef.current = s.channel(options.bufferSize ?? 0);
|
|
119
|
+
}
|
|
120
|
+
const [latest, setLatest] = useState(void 0);
|
|
121
|
+
const [history, setHistory] = useState([]);
|
|
122
|
+
const [isClosed, setIsClosed] = useState(false);
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const ch = chRef.current;
|
|
125
|
+
const receiveLoop = async () => {
|
|
126
|
+
for await (const value of ch) {
|
|
127
|
+
setLatest(value);
|
|
128
|
+
setHistory((prev) => [...prev.slice(-(options.historySize ?? 100)), value]);
|
|
129
|
+
}
|
|
130
|
+
setIsClosed(true);
|
|
131
|
+
};
|
|
132
|
+
receiveLoop();
|
|
133
|
+
return () => {
|
|
134
|
+
ch.close();
|
|
135
|
+
};
|
|
136
|
+
}, [options.historySize]);
|
|
137
|
+
const send = useCallback(
|
|
138
|
+
async (value) => {
|
|
139
|
+
await chRef.current.send(value);
|
|
140
|
+
},
|
|
141
|
+
[]
|
|
142
|
+
);
|
|
143
|
+
const close = useCallback(() => {
|
|
144
|
+
chRef.current.close();
|
|
145
|
+
}, []);
|
|
146
|
+
return {
|
|
147
|
+
latest,
|
|
148
|
+
history,
|
|
149
|
+
isClosed,
|
|
150
|
+
send,
|
|
151
|
+
close
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function useBroadcast() {
|
|
155
|
+
const [latest, setLatest] = useState(void 0);
|
|
156
|
+
const bcRef = useRef(null);
|
|
157
|
+
const listenersRef = useRef(/* @__PURE__ */ new Set());
|
|
158
|
+
if (!bcRef.current) {
|
|
159
|
+
bcRef.current = new BroadcastChannel();
|
|
160
|
+
}
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const bc = bcRef.current;
|
|
163
|
+
const receiveLoop = async () => {
|
|
164
|
+
for await (const value of bc.subscribe()) {
|
|
165
|
+
setLatest(value);
|
|
166
|
+
listenersRef.current.forEach((listener) => listener(value));
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
receiveLoop();
|
|
170
|
+
return () => {
|
|
171
|
+
bc.close();
|
|
172
|
+
};
|
|
173
|
+
}, []);
|
|
174
|
+
const subscribe = useCallback((callback) => {
|
|
175
|
+
listenersRef.current.add(callback);
|
|
176
|
+
return {
|
|
177
|
+
unsubscribe: () => {
|
|
178
|
+
listenersRef.current.delete(callback);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}, []);
|
|
182
|
+
const broadcastFn = useCallback((value) => {
|
|
183
|
+
bcRef.current.send(value).catch(() => {
|
|
184
|
+
});
|
|
185
|
+
setLatest(value);
|
|
186
|
+
}, []);
|
|
187
|
+
return {
|
|
188
|
+
latest,
|
|
189
|
+
subscribe,
|
|
190
|
+
broadcast: broadcastFn
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function usePolling(factory, options) {
|
|
194
|
+
const s = useScope({ name: "usePolling" });
|
|
195
|
+
const [data, setData] = useState(void 0);
|
|
196
|
+
const [error, setError] = useState(void 0);
|
|
197
|
+
const [isPolling, setIsPolling] = useState(false);
|
|
198
|
+
const [pollCount, setPollCount] = useState(0);
|
|
199
|
+
const pollerRef = useRef(null);
|
|
200
|
+
const start = useCallback(() => {
|
|
201
|
+
if (!isPolling && !pollerRef.current) {
|
|
202
|
+
pollerRef.current = s.poll(
|
|
203
|
+
async () => {
|
|
204
|
+
try {
|
|
205
|
+
const value = await factory();
|
|
206
|
+
return { success: true, value };
|
|
207
|
+
} catch (e) {
|
|
208
|
+
return { success: false, error: e };
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
(result) => {
|
|
212
|
+
if (result.success) {
|
|
213
|
+
setData(result.value);
|
|
214
|
+
setPollCount((c) => c + 1);
|
|
215
|
+
} else {
|
|
216
|
+
setError(result.error instanceof Error ? result.error : new Error(String(result.error)));
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
{ interval: options.interval }
|
|
220
|
+
);
|
|
221
|
+
setIsPolling(true);
|
|
222
|
+
}
|
|
223
|
+
}, [s, factory, options.interval, isPolling]);
|
|
224
|
+
const stop = useCallback(() => {
|
|
225
|
+
if (isPolling && pollerRef.current) {
|
|
226
|
+
pollerRef.current.stop();
|
|
227
|
+
setIsPolling(false);
|
|
228
|
+
pollerRef.current = null;
|
|
229
|
+
}
|
|
230
|
+
}, [isPolling]);
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
if (options.immediate !== false) {
|
|
233
|
+
start();
|
|
234
|
+
}
|
|
235
|
+
return () => {
|
|
236
|
+
stop();
|
|
237
|
+
};
|
|
238
|
+
}, []);
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (!options.continueOnHidden && typeof document !== "undefined") {
|
|
241
|
+
const handler = () => {
|
|
242
|
+
if (document.hidden) {
|
|
243
|
+
stop();
|
|
244
|
+
} else if (options.immediate !== false) {
|
|
245
|
+
start();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
document.addEventListener("visibilitychange", handler);
|
|
249
|
+
return () => document.removeEventListener("visibilitychange", handler);
|
|
250
|
+
}
|
|
251
|
+
}, [options.continueOnHidden, options.immediate, start, stop]);
|
|
252
|
+
return {
|
|
253
|
+
data,
|
|
254
|
+
error,
|
|
255
|
+
isPolling,
|
|
256
|
+
pollCount,
|
|
257
|
+
start,
|
|
258
|
+
stop
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export { useBroadcast, useChannel, useParallel, usePolling, useScope, useTask };
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@go-go-scope/adapter-react",
|
|
3
|
+
"version": "2.7.0",
|
|
4
|
+
"description": "React hooks for go-go-scope",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.mts",
|
|
11
|
+
"default": "./dist/index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"go-go-scope",
|
|
16
|
+
"react",
|
|
17
|
+
"hooks",
|
|
18
|
+
"react-hooks",
|
|
19
|
+
"concurrency"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24.0.0"
|
|
23
|
+
},
|
|
24
|
+
"author": "thelinuxlich",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
28
|
+
"go-go-scope": "2.7.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@biomejs/biome": "^2.4.4",
|
|
32
|
+
"@testing-library/react": "^16.0.0",
|
|
33
|
+
"@types/node": "^24",
|
|
34
|
+
"@types/react": "^18.3.0",
|
|
35
|
+
"jsdom": "^24.0.0",
|
|
36
|
+
"pkgroll": "^2.26.3",
|
|
37
|
+
"react": "^18.3.0",
|
|
38
|
+
"react-dom": "^18.3.0",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"vitest": "^4.0.18",
|
|
41
|
+
"go-go-scope": "2.7.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "pkgroll --clean-dist",
|
|
45
|
+
"lint": "biome check --write src/",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"clean": "rm -rf dist",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
}
|
|
50
|
+
}
|