@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 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
@@ -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
+ }