preact-missing-hooks 1.2.0 → 2.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.
@@ -0,0 +1,176 @@
1
+ /**
2
+ * useWebRTCIP – detect local/public IPs via WebRTC ICE candidates and STUN.
3
+ * Not highly reliable; use as first-priority hint and fall back to a public IP API (e.g. ipapi.co) if needed.
4
+ * @module useWebRTCIP
5
+ */
6
+
7
+ import { useEffect, useState, useRef } from 'preact/hooks';
8
+
9
+ /** IPv4 regex for ICE candidate strings (captures dotted-decimal). */
10
+ const IPV4_REGEX = /\b(?:25[0-5]|2[0-4]\d|1?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|1?\d{1,2})){3}\b/g;
11
+
12
+ const DEFAULT_STUN_SERVERS: string[] = ['stun:stun.l.google.com:19302'];
13
+ const DEFAULT_TIMEOUT_MS = 3000;
14
+
15
+ export interface UseWebRTCIPOptions {
16
+ /** STUN server URLs (default: Google STUN). */
17
+ stunServers?: string[];
18
+ /** Stop gathering after this many ms (default: 3000). */
19
+ timeout?: number;
20
+ /** Called once per newly detected IP (no duplicates). */
21
+ onDetect?: (ip: string) => void;
22
+ }
23
+
24
+ export interface UseWebRTCIPReturn {
25
+ /** Unique IPv4 addresses found from ICE candidates. */
26
+ ips: string[];
27
+ /** True while ICE gathering is in progress. */
28
+ loading: boolean;
29
+ /** Error message if WebRTC is unavailable or detection fails. */
30
+ error: string | null;
31
+ }
32
+
33
+ function isSSR(): boolean {
34
+ return typeof window === 'undefined';
35
+ }
36
+
37
+ function isWebRTCAvailable(): boolean {
38
+ return typeof RTCPeerConnection !== 'undefined';
39
+ }
40
+
41
+ /**
42
+ * Extracts IPv4 addresses from an ICE candidate string.
43
+ * Filters out common non-public/local patterns (e.g. 0.0.0.0) if desired; currently returns all matches.
44
+ */
45
+ function extractIPv4FromCandidate(candidate: string): string[] {
46
+ const matches = candidate.match(IPV4_REGEX);
47
+ return matches ? [...matches] : [];
48
+ }
49
+
50
+ /**
51
+ * Attempts to detect client IP addresses using WebRTC ICE candidates and a STUN server.
52
+ * Works frontend-only (no backend). Not guaranteed to return a public IP; use as a hint and
53
+ * fall back to a public IP API (e.g. ipapi.co, ip-api.com) if you need reliability.
54
+ *
55
+ * @param options - Optional: stunServers, timeout (ms), onDetect(ip) callback.
56
+ * @returns { ips, loading, error } – unique IPv4s, loading flag, and error message.
57
+ *
58
+ * @example
59
+ * const { ips, loading, error } = useWebRTCIP({
60
+ * timeout: 5000,
61
+ * onDetect: (ip) => console.log('Detected:', ip),
62
+ * })
63
+ * // If ips is empty and error is set, fall back to: fetch('https://api.ipify.org?format=json')
64
+ */
65
+ export function useWebRTCIP(options: UseWebRTCIPOptions = {}): UseWebRTCIPReturn {
66
+ const {
67
+ stunServers = DEFAULT_STUN_SERVERS,
68
+ timeout: timeoutMs = DEFAULT_TIMEOUT_MS,
69
+ onDetect,
70
+ } = options;
71
+
72
+ const [ips, setIps] = useState<string[]>([]);
73
+ const [loading, setLoading] = useState(true);
74
+ const [error, setError] = useState<string | null>(null);
75
+
76
+ const pcRef = useRef<RTCPeerConnection | null>(null);
77
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
78
+ const reportedRef = useRef<Set<string>>(new Set());
79
+ const onDetectRef = useRef(onDetect);
80
+ onDetectRef.current = onDetect;
81
+
82
+ useEffect(() => {
83
+ if (isSSR()) {
84
+ setLoading(false);
85
+ setError('WebRTC IP detection is not available during SSR');
86
+ return;
87
+ }
88
+
89
+ if (!isWebRTCAvailable()) {
90
+ setLoading(false);
91
+ setError('RTCPeerConnection is not available');
92
+ return;
93
+ }
94
+
95
+ const reported = new Set<string>();
96
+ reportedRef.current = reported;
97
+
98
+ const finish = () => {
99
+ if (timeoutRef.current) {
100
+ clearTimeout(timeoutRef.current);
101
+ timeoutRef.current = null;
102
+ }
103
+ if (pcRef.current) {
104
+ pcRef.current.close();
105
+ pcRef.current = null;
106
+ }
107
+ setLoading(false);
108
+ };
109
+
110
+ const addIP = (ip: string) => {
111
+ if (reported.has(ip)) return;
112
+ reported.add(ip);
113
+ setIps((prev) => {
114
+ const next = [...prev, ip];
115
+ return next;
116
+ });
117
+ onDetectRef.current?.(ip);
118
+ };
119
+
120
+ try {
121
+ const pc = new RTCPeerConnection({
122
+ iceServers: [{ urls: stunServers }],
123
+ });
124
+ pcRef.current = pc;
125
+
126
+ pc.onicecandidate = (event) => {
127
+ const c = event.candidate;
128
+ if (!c || !c.candidate) return;
129
+ const found = extractIPv4FromCandidate(c.candidate);
130
+ found.forEach(addIP);
131
+ };
132
+
133
+ pc.createDataChannel('');
134
+
135
+ pc.createOffer()
136
+ .then((offer) => pc.setLocalDescription(offer))
137
+ .catch((err) => {
138
+ setError(err instanceof Error ? err.message : 'Failed to create offer');
139
+ finish();
140
+ });
141
+
142
+ timeoutRef.current = setTimeout(() => finish(), timeoutMs);
143
+ } catch (err) {
144
+ setError(err instanceof Error ? err.message : 'WebRTC setup failed');
145
+ finish();
146
+ }
147
+
148
+ return () => {
149
+ finish();
150
+ };
151
+ }, [stunServers.join(','), timeoutMs]);
152
+
153
+ return { ips, loading, error };
154
+ }
155
+
156
+ /*
157
+ * Example usage (Preact component):
158
+ *
159
+ * function MyIPDisplay() {
160
+ * const { ips, loading, error } = useWebRTCIP({
161
+ * timeout: 4000,
162
+ * onDetect: (ip) => { / * optional: e.g. send to analytics * / },
163
+ * })
164
+ *
165
+ * if (loading) return <p>Detecting IP…</p>
166
+ * if (error) return <p>WebRTC failed: {error}. Try fallback API.</p>
167
+ * return <p>IPs (WebRTC): {ips.join(', ') || 'None'}</p>
168
+ * }
169
+ *
170
+ * Fallback to public IP API when WebRTC fails or returns empty:
171
+ * const [apiIP, setApiIP] = useState<string | null>(null)
172
+ * useEffect(() => {
173
+ * if (!loading && ips.length === 0 && error)
174
+ * fetch('https://api.ipify.org?format=json').then(r => r.json()).then(d => setApiIP(d.ip))
175
+ * }, [loading, ips.length, error])
176
+ */
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ * Polyfill IndexedDB for Node/jsdom (must run before hook code).
4
+ */
5
+ import 'fake-indexeddb/auto';
6
+
7
+ /** @jsx h */
8
+ import { h } from 'preact';
9
+ import { render, waitFor } from '@testing-library/preact';
10
+ import '@testing-library/jest-dom';
11
+ import { useIndexedDB } from '@/useIndexedDB';
12
+
13
+ const DB_NAME = 'test-db-' + Math.random().toString(36).slice(2);
14
+ const DB_VERSION = 1;
15
+ const config = {
16
+ name: DB_NAME,
17
+ version: DB_VERSION,
18
+ tables: {
19
+ users: {
20
+ keyPath: 'id',
21
+ autoIncrement: true,
22
+ indexes: ['email'],
23
+ },
24
+ settings: {
25
+ keyPath: 'key',
26
+ },
27
+ },
28
+ };
29
+
30
+ describe('useIndexedDB', () => {
31
+ it('opens database and sets isReady', async () => {
32
+ let dbRef: ReturnType<typeof useIndexedDB>['db'] = null;
33
+ function TestComponent() {
34
+ const result = useIndexedDB(config);
35
+ dbRef = result.db;
36
+ return (
37
+ <div>
38
+ <span data-testid="ready">{String(result.isReady)}</span>
39
+ <span data-testid="error">{result.error?.message ?? 'none'}</span>
40
+ </div>
41
+ );
42
+ }
43
+ render(<TestComponent />);
44
+ expect(dbRef).toBeNull();
45
+ await waitFor(() => {
46
+ expect(dbRef).not.toBeNull();
47
+ });
48
+ const readyEl = document.querySelector('[data-testid="ready"]');
49
+ expect(readyEl?.textContent).toBe('true');
50
+ });
51
+
52
+ it('hasTable returns true for defined tables', async () => {
53
+ let dbRef: ReturnType<typeof useIndexedDB>['db'] = null;
54
+ function TestComponent() {
55
+ const result = useIndexedDB(config);
56
+ dbRef = result.db;
57
+ return <div data-testid="ready">{String(result.isReady)}</div>;
58
+ }
59
+ render(<TestComponent />);
60
+ await waitFor(() => expect(dbRef).not.toBeNull());
61
+ expect(dbRef!.hasTable('users')).toBe(true);
62
+ expect(dbRef!.hasTable('settings')).toBe(true);
63
+ expect(dbRef!.hasTable('nonexistent')).toBe(false);
64
+ });
65
+
66
+ it('table insert and count', async () => {
67
+ let dbRef: ReturnType<typeof useIndexedDB>['db'] = null;
68
+ function TestComponent() {
69
+ const result = useIndexedDB(config);
70
+ dbRef = result.db;
71
+ return <div>{String(result.isReady)}</div>;
72
+ }
73
+ render(<TestComponent />);
74
+ await waitFor(() => expect(dbRef).not.toBeNull());
75
+ const users = dbRef!.table('users');
76
+ const key = await users.insert({ email: 'a@b.com', name: 'Alice' });
77
+ expect(key).toBe(1);
78
+ const n = await users.count();
79
+ expect(n).toBe(1);
80
+ });
81
+
82
+ it('table query with filter', async () => {
83
+ let dbRef: ReturnType<typeof useIndexedDB>['db'] = null;
84
+ function TestComponent() {
85
+ const result = useIndexedDB(config);
86
+ dbRef = result.db;
87
+ return <div>{String(result.isReady)}</div>;
88
+ }
89
+ render(<TestComponent />);
90
+ await waitFor(() => expect(dbRef).not.toBeNull());
91
+ const users = dbRef!.table('users');
92
+ await users.clear();
93
+ await users.insert({ email: 'a@b.com' });
94
+ await users.insert({ email: 'b@b.com' });
95
+ await users.insert({ email: 'a@c.com' });
96
+ const found = await users.query((item: { email: string }) => item.email.startsWith('a@'));
97
+ expect(found).toHaveLength(2);
98
+ const emails = found.map((x: { email: string }) => x.email).sort();
99
+ expect(emails).toEqual(['a@b.com', 'a@c.com']);
100
+ });
101
+
102
+ it('table exists, update, delete', async () => {
103
+ let dbRef: ReturnType<typeof useIndexedDB>['db'] = null;
104
+ function TestComponent() {
105
+ const result = useIndexedDB(config);
106
+ dbRef = result.db;
107
+ return <div>{String(result.isReady)}</div>;
108
+ }
109
+ render(<TestComponent />);
110
+ await waitFor(() => expect(dbRef).not.toBeNull());
111
+ const users = dbRef!.table('users');
112
+ await users.clear();
113
+ const key = await users.insert({ email: 'x@b.com', name: 'X' });
114
+ let ok = await users.exists(key);
115
+ expect(ok).toBe(true);
116
+ await users.update(key, { name: 'X Updated' });
117
+ await users.delete(key);
118
+ ok = await users.exists(key);
119
+ expect(ok).toBe(false);
120
+ });
121
+
122
+ it('table upsert and bulkInsert', async () => {
123
+ let dbRef: ReturnType<typeof useIndexedDB>['db'] = null;
124
+ function TestComponent() {
125
+ const result = useIndexedDB(config);
126
+ dbRef = result.db;
127
+ return <div>{String(result.isReady)}</div>;
128
+ }
129
+ render(<TestComponent />);
130
+ await waitFor(() => expect(dbRef).not.toBeNull());
131
+ const settings = dbRef!.table('settings');
132
+ await settings.clear();
133
+ await settings.upsert({ key: 'theme', value: 'dark' });
134
+ let n = await settings.count();
135
+ expect(n).toBe(1);
136
+ await settings.upsert({ key: 'theme', value: 'light' });
137
+ n = await settings.count();
138
+ expect(n).toBe(1);
139
+ const keys = await settings.bulkInsert([
140
+ { key: 'a', value: 1 },
141
+ { key: 'b', value: 2 },
142
+ ]);
143
+ expect(keys).toHaveLength(2);
144
+ n = await settings.count();
145
+ expect(n).toBe(3);
146
+ });
147
+
148
+ it('table clear', async () => {
149
+ let dbRef: ReturnType<typeof useIndexedDB>['db'] = null;
150
+ function TestComponent() {
151
+ const result = useIndexedDB(config);
152
+ dbRef = result.db;
153
+ return <div>{String(result.isReady)}</div>;
154
+ }
155
+ render(<TestComponent />);
156
+ await waitFor(() => expect(dbRef).not.toBeNull());
157
+ const settings = dbRef!.table('settings');
158
+ await settings.upsert({ key: 'toClear', value: 1 });
159
+ await settings.clear();
160
+ const n = await settings.count();
161
+ expect(n).toBe(0);
162
+ });
163
+
164
+ it('optional onSuccess and onError callbacks', async () => {
165
+ let dbRef: ReturnType<typeof useIndexedDB>['db'] = null;
166
+ function TestComponent() {
167
+ const result = useIndexedDB(config);
168
+ dbRef = result.db;
169
+ return <div>{String(result.isReady)}</div>;
170
+ }
171
+ render(<TestComponent />);
172
+ await waitFor(() => expect(dbRef).not.toBeNull());
173
+ const users = dbRef!.table('users');
174
+ let onSuccessCalled = false;
175
+ await users.insert({ email: 'cb@b.com' }, {
176
+ onSuccess: () => { onSuccessCalled = true; },
177
+ });
178
+ expect(onSuccessCalled).toBe(true);
179
+ let onErrorCalled = false;
180
+ await users.update(99999, { email: 'x' }, {
181
+ onError: () => { onErrorCalled = true; },
182
+ }).catch(() => { });
183
+ expect(onErrorCalled).toBe(true);
184
+ });
185
+
186
+ it('transaction runs and commits', async () => {
187
+ let dbRef: ReturnType<typeof useIndexedDB>['db'] = null;
188
+ function TestComponent() {
189
+ const result = useIndexedDB(config);
190
+ dbRef = result.db;
191
+ return <div>{String(result.isReady)}</div>;
192
+ }
193
+ render(<TestComponent />);
194
+ await waitFor(() => expect(dbRef).not.toBeNull());
195
+ await dbRef!.transaction(['users', 'settings'], 'readwrite', async (tx) => {
196
+ await tx.table('users').insert({ email: 'tx@b.com' });
197
+ await tx.table('settings').upsert({ key: 'fromTx', value: true });
198
+ });
199
+ const users = dbRef!.table('users');
200
+ const found = await users.query((item: { email: string }) => item.email === 'tx@b.com');
201
+ expect(found.length).toBeGreaterThanOrEqual(1);
202
+ expect(found.some((x: { email: string }) => x.email === 'tx@b.com')).toBe(true);
203
+ const settings = dbRef!.table('settings');
204
+ const fromTx = await settings.query((item: { key: string }) => item.key === 'fromTx');
205
+ expect(fromTx).toHaveLength(1);
206
+ expect((fromTx[0] as { value: boolean }).value).toBe(true);
207
+ });
208
+ });
@@ -0,0 +1,267 @@
1
+ /** @jsx h */
2
+ import { h } from 'preact'
3
+ import { render, waitFor } from '@testing-library/preact'
4
+ import '@testing-library/jest-dom'
5
+ import { useThreadedWorker } from '@/useThreadedWorker'
6
+
7
+ /** Worker that resolves after delay with the input value (for order/concurrency tests). */
8
+ function delayedWorker<T>(delayMs: number) {
9
+ return (data: T): Promise<T> =>
10
+ new Promise((resolve) => setTimeout(() => resolve(data), delayMs))
11
+ }
12
+
13
+ describe('useThreadedWorker', () => {
14
+ describe('sequential mode', () => {
15
+ it('runs one task at a time and returns result', async () => {
16
+ const worker = vi.fn().mockResolvedValue('done')
17
+ let api: ReturnType<typeof useThreadedWorker<string, string>>
18
+
19
+ function TestComponent() {
20
+ api = useThreadedWorker(worker, { mode: 'sequential' })
21
+ return (
22
+ <div>
23
+ <span data-testid="loading">{String(api!.loading)}</span>
24
+ <span data-testid="result">{api!.result ?? ''}</span>
25
+ <span data-testid="queueSize">{api!.queueSize}</span>
26
+ </div>
27
+ )
28
+ }
29
+
30
+ render(<TestComponent />)
31
+ expect(api!.loading).toBe(false)
32
+ expect(api!.result).toBeUndefined()
33
+ expect(api!.queueSize).toBe(0)
34
+
35
+ const p = api!.run('a')
36
+ await waitFor(() => expect(api!.loading).toBe(true))
37
+ await waitFor(() => expect(api!.queueSize).toBe(1))
38
+
39
+ await waitFor(() => expect(worker).toHaveBeenCalledWith('a'))
40
+ await p
41
+ await waitFor(() => {
42
+ expect(api!.result).toBe('done')
43
+ expect(api!.loading).toBe(false)
44
+ expect(api!.queueSize).toBe(0)
45
+ })
46
+ })
47
+
48
+ it('processes tasks in priority order (lower number first)', async () => {
49
+ const order: number[] = []
50
+ const worker = (data: { id: number }) =>
51
+ new Promise<number>((resolve) => {
52
+ order.push(data.id)
53
+ setTimeout(() => resolve(data.id), 10)
54
+ })
55
+
56
+ let api: ReturnType<typeof useThreadedWorker<{ id: number }, number>>
57
+
58
+ function TestComponent() {
59
+ api = useThreadedWorker(worker, { mode: 'sequential' })
60
+ return <div data-testid="queueSize">{api!.queueSize}</div>
61
+ }
62
+
63
+ render(<TestComponent />)
64
+ // Enqueue all in one tick; processNext runs in microtask so queue is sorted by priority.
65
+ // Submission order: 100(p2), 200(p1), 300(p3). Execution order must be 200, 100, 300.
66
+ api!.run({ id: 100 }, { priority: 2 })
67
+ api!.run({ id: 200 }, { priority: 1 })
68
+ api!.run({ id: 300 }, { priority: 3 })
69
+
70
+ await waitFor(() => expect(order).toHaveLength(3))
71
+ expect(order).toEqual([200, 100, 300]) // priority 1, then 2, then 3
72
+ })
73
+
74
+ it('FIFO within same priority', async () => {
75
+ const order: number[] = []
76
+ const worker = (data: number) =>
77
+ new Promise<number>((resolve) => {
78
+ order.push(data)
79
+ setTimeout(() => resolve(data), 5)
80
+ })
81
+
82
+ let api: ReturnType<typeof useThreadedWorker<number, number>>
83
+
84
+ function TestComponent() {
85
+ api = useThreadedWorker(worker, { mode: 'sequential' })
86
+ return null
87
+ }
88
+
89
+ render(<TestComponent />)
90
+ api!.run(1, { priority: 1 })
91
+ api!.run(2, { priority: 1 })
92
+ api!.run(3, { priority: 1 })
93
+
94
+ await waitFor(() => expect(order).toEqual([1, 2, 3]))
95
+ })
96
+
97
+ it('run() returns a Promise that resolves with worker result', async () => {
98
+ const worker = (x: number) => Promise.resolve(x * 2)
99
+ let api: ReturnType<typeof useThreadedWorker<number, number>>
100
+
101
+ function TestComponent() {
102
+ api = useThreadedWorker(worker, { mode: 'sequential' })
103
+ return null
104
+ }
105
+
106
+ render(<TestComponent />)
107
+ const result = await api!.run(21)
108
+ expect(result).toBe(42)
109
+ })
110
+
111
+ it('sets error when worker rejects', async () => {
112
+ const err = new Error('worker failed')
113
+ const worker = () => Promise.reject(err)
114
+ let api: ReturnType<typeof useThreadedWorker<void, void>>
115
+
116
+ function TestComponent() {
117
+ api = useThreadedWorker(worker, { mode: 'sequential' })
118
+ return (
119
+ <div>
120
+ <span data-testid="error">{(api!.error as Error)?.message ?? ''}</span>
121
+ </div>
122
+ )
123
+ }
124
+
125
+ render(<TestComponent />)
126
+ api!.run(undefined).catch(() => { })
127
+ await waitFor(() => expect(api!.error).toBe(err))
128
+ expect((api!.error as Error).message).toBe('worker failed')
129
+ })
130
+
131
+ it('clearQueue rejects pending tasks and leaves running task to finish', async () => {
132
+ const worker = delayedWorker<string>(100)
133
+ let api: ReturnType<typeof useThreadedWorker<string, string>>
134
+
135
+ function TestComponent() {
136
+ api = useThreadedWorker(worker, { mode: 'sequential' })
137
+ return null
138
+ }
139
+
140
+ render(<TestComponent />)
141
+ const first = api!.run('running')
142
+ const pending1 = api!.run('pending1')
143
+ const pending2 = api!.run('pending2')
144
+ await waitFor(() => expect(api!.queueSize).toBeGreaterThanOrEqual(2))
145
+
146
+ api!.clearQueue()
147
+ await expect(pending1.catch((e: Error) => e.message)).resolves.toBe(
148
+ 'Task cleared from queue'
149
+ )
150
+ await expect(pending2.catch((e: Error) => e.message)).resolves.toBe(
151
+ 'Task cleared from queue'
152
+ )
153
+ await expect(first).resolves.toBe('running')
154
+ })
155
+
156
+ it('terminate clears queue and rejects new run()', async () => {
157
+ const worker = (x: string) => Promise.resolve(x)
158
+ let api: ReturnType<typeof useThreadedWorker<string, string>>
159
+
160
+ function TestComponent() {
161
+ api = useThreadedWorker(worker, { mode: 'sequential' })
162
+ return null
163
+ }
164
+
165
+ render(<TestComponent />)
166
+ api!.terminate()
167
+ await expect(api!.run('x').catch((e: Error) => e.message)).resolves.toBe(
168
+ 'Worker is terminated'
169
+ )
170
+ })
171
+ })
172
+
173
+ describe('parallel mode', () => {
174
+ it('runs up to concurrency tasks at once', async () => {
175
+ let concurrent = 0
176
+ let maxSeen = 0
177
+ const worker = async (id: number) => {
178
+ concurrent += 1
179
+ maxSeen = Math.max(maxSeen, concurrent)
180
+ await new Promise((r) => setTimeout(r, 30))
181
+ concurrent -= 1
182
+ return id
183
+ }
184
+
185
+ let api: ReturnType<typeof useThreadedWorker<number, number>>
186
+
187
+ function TestComponent() {
188
+ api = useThreadedWorker(worker, { mode: 'parallel', concurrency: 3 })
189
+ return <span data-testid="queueSize">{api!.queueSize}</span>
190
+ }
191
+
192
+ render(<TestComponent />)
193
+ api!.run(1)
194
+ api!.run(2)
195
+ api!.run(3)
196
+ api!.run(4)
197
+ api!.run(5)
198
+
199
+ await waitFor(() => expect(maxSeen).toBe(3))
200
+ expect(api!.queueSize).toBeGreaterThanOrEqual(0)
201
+ await waitFor(() => expect(api!.loading).toBe(false))
202
+ expect(api!.result).toBe(5) // last to finish in typical scheduling
203
+ })
204
+
205
+ it('processes all tasks in parallel and drains queue', async () => {
206
+ const worker = (data: { id: number }) =>
207
+ new Promise<number>((resolve) => setTimeout(() => resolve(data.id), 20))
208
+
209
+ let api: ReturnType<typeof useThreadedWorker<{ id: number }, number>>
210
+
211
+ function TestComponent() {
212
+ api = useThreadedWorker(worker, { mode: 'parallel', concurrency: 2 })
213
+ return null
214
+ }
215
+
216
+ render(<TestComponent />)
217
+ const [r1, r2, r3] = await Promise.all([
218
+ api!.run({ id: 1 }, { priority: 2 }),
219
+ api!.run({ id: 2 }, { priority: 1 }),
220
+ api!.run({ id: 3 }, { priority: 1 }),
221
+ ])
222
+ expect([r1, r2, r3].sort((a, b) => a - b)).toEqual([1, 2, 3])
223
+ await waitFor(() => {
224
+ expect(api!.queueSize).toBe(0)
225
+ expect(api!.loading).toBe(false)
226
+ })
227
+ })
228
+
229
+ it('queueSize reflects pending + running', async () => {
230
+ const worker = delayedWorker<number>(40)
231
+ let api: ReturnType<typeof useThreadedWorker<number, number>>
232
+
233
+ function TestComponent() {
234
+ api = useThreadedWorker(worker, { mode: 'parallel', concurrency: 2 })
235
+ return <span data-testid="queueSize">{api!.queueSize}</span>
236
+ }
237
+
238
+ render(<TestComponent />)
239
+ api!.run(1)
240
+ api!.run(2)
241
+ api!.run(3)
242
+ await waitFor(() => expect(api!.queueSize).toBe(3))
243
+ await waitFor(() => expect(api!.queueSize).toBeLessThanOrEqual(2))
244
+ await waitFor(() => expect(api!.queueSize).toBe(0))
245
+ })
246
+
247
+ it('run() Promise resolves with correct result in parallel', async () => {
248
+ const worker = (x: number) => Promise.resolve(x * 10)
249
+ let api: ReturnType<typeof useThreadedWorker<number, number>>
250
+
251
+ function TestComponent() {
252
+ api = useThreadedWorker(worker, { mode: 'parallel', concurrency: 2 })
253
+ return null
254
+ }
255
+
256
+ render(<TestComponent />)
257
+ const [a, b, c] = await Promise.all([
258
+ api!.run(1),
259
+ api!.run(2),
260
+ api!.run(3),
261
+ ])
262
+ expect(a).toBe(10)
263
+ expect(b).toBe(20)
264
+ expect(c).toBe(30)
265
+ })
266
+ })
267
+ })