preact-missing-hooks 3.0.0 → 4.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.
Files changed (79) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.husky/pre-push +1 -0
  3. package/.prettierignore +3 -0
  4. package/.prettierrc +6 -0
  5. package/Readme.md +179 -131
  6. package/dist/entry.cjs +21 -0
  7. package/dist/entry.js +2 -0
  8. package/dist/entry.js.map +1 -0
  9. package/dist/entry.modern.mjs +2 -0
  10. package/dist/entry.modern.mjs.map +1 -0
  11. package/dist/entry.module.js +2 -0
  12. package/dist/entry.module.js.map +1 -0
  13. package/dist/entry.umd.js +2 -0
  14. package/dist/entry.umd.js.map +1 -0
  15. package/dist/index.d.ts +13 -12
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.modern.mjs +2 -0
  19. package/dist/index.modern.mjs.map +1 -0
  20. package/dist/index.module.js +1 -1
  21. package/dist/index.module.js.map +1 -1
  22. package/dist/index.umd.js +1 -1
  23. package/dist/index.umd.js.map +1 -1
  24. package/dist/indexedDB/dbController.d.ts +2 -2
  25. package/dist/indexedDB/index.d.ts +6 -6
  26. package/dist/indexedDB/openDB.d.ts +1 -1
  27. package/dist/indexedDB/tableController.d.ts +1 -1
  28. package/dist/indexedDB/types.d.ts +1 -2
  29. package/dist/react.js +1 -0
  30. package/dist/react.modern.mjs +1 -0
  31. package/dist/react.module.js +1 -0
  32. package/dist/react.umd.js +1 -0
  33. package/dist/useEventBus.d.ts +1 -1
  34. package/dist/useIndexedDB.d.ts +3 -3
  35. package/dist/useMutationObserver.d.ts +1 -1
  36. package/dist/useNetworkState.d.ts +3 -3
  37. package/dist/usePreferredTheme.d.ts +1 -1
  38. package/dist/useRageClick.d.ts +1 -1
  39. package/dist/useThreadedWorker.d.ts +1 -1
  40. package/dist/useTransition.d.ts +4 -1
  41. package/dist/useWorkerNotifications.d.ts +57 -0
  42. package/dist/useWrappedChildren.d.ts +3 -3
  43. package/{demo → docs}/index.html +56 -0
  44. package/{demo → docs}/main.js +437 -312
  45. package/eslint.config.mjs +10 -0
  46. package/package.json +65 -6
  47. package/scripts/generate-entry.cjs +34 -0
  48. package/src/index.ts +13 -12
  49. package/src/indexedDB/dbController.ts +101 -92
  50. package/src/indexedDB/index.ts +16 -11
  51. package/src/indexedDB/openDB.ts +49 -49
  52. package/src/indexedDB/requestToPromise.ts +17 -16
  53. package/src/indexedDB/tableController.ts +331 -257
  54. package/src/indexedDB/types.ts +35 -35
  55. package/src/useClipboard.ts +99 -97
  56. package/src/useEventBus.ts +39 -36
  57. package/src/useIndexedDB.ts +111 -111
  58. package/src/useMutationObserver.ts +26 -26
  59. package/src/useNetworkState.ts +124 -122
  60. package/src/usePreferredTheme.ts +68 -68
  61. package/src/useRageClick.ts +103 -103
  62. package/src/useThreadedWorker.ts +165 -165
  63. package/src/useTransition.ts +22 -19
  64. package/src/useWasmCompute.ts +209 -204
  65. package/src/useWebRTCIP.ts +181 -176
  66. package/src/useWorkerNotifications.ts +203 -0
  67. package/src/useWrappedChildren.ts +72 -58
  68. package/tests/react-adapter.tsx +12 -0
  69. package/tests/setup-react.ts +4 -0
  70. package/tests/useClipboard.test.tsx +4 -2
  71. package/tests/useThreadedWorker.test.tsx +3 -1
  72. package/tests/useWasmCompute.test.tsx +1 -1
  73. package/tests/useWebRTCIP.test.tsx +3 -1
  74. package/tests/useWorkerNotifications.test.tsx +170 -0
  75. package/vite.config.ts +11 -4
  76. package/vitest.config.preact.ts +20 -0
  77. package/vitest.config.react.ts +36 -0
  78. package/vitest.workspace.ts +6 -0
  79. /package/{demo → docs}/add.wasm +0 -0
@@ -0,0 +1 @@
1
+ npm run lint && npm run format
@@ -0,0 +1 @@
1
+ npm run test && npm run size
@@ -0,0 +1,3 @@
1
+ dist
2
+ node_modules
3
+ *.min.js
package/.prettierrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": false,
4
+ "tabWidth": 2,
5
+ "trailingComma": "es5"
6
+ }
package/Readme.md CHANGED
@@ -32,10 +32,11 @@ A lightweight, extendable collection of React-like hooks for Preact, including u
32
32
  - **`useThreadedWorker`** — Run async work in a queue with **sequential** (single worker, priority-ordered) or **parallel** (worker pool) mode. Optional priority (1 = highest); FIFO within same priority.
33
33
  - **`useIndexedDB`** — IndexedDB abstraction with database/table init, insert, update, delete, exists, query (cursor + filter), upsert, bulk insert, clear, count, and full transaction support. Singleton connection, Promise-based API, optional `onSuccess`/`onError` callbacks.
34
34
  - **`useWebRTCIP`** — Detects client IP addresses using WebRTC ICE candidates and a STUN server (frontend-only). **Not highly reliable**; use as a first-priority hint and fall back to a public IP API (e.g. [ipapi.co](https://ipapi.co), [ipify](https://www.ipify.org), [ip-api.com](https://ip-api.com)) when it fails or returns empty.
35
- - **`useWasmCompute`** — Runs WebAssembly computation off the main thread via a Web Worker. Flow: Preact Component → useWasmCompute() → Web Worker → WASM Module → return result. Validates environment (browser, Worker, WebAssembly) and returns `compute(input)`, `result`, `loading`, `error`, `ready`.
35
+ - **`useWasmCompute`** — Runs WebAssembly computation off the main thread via a Web Worker. Validates environment (browser, Worker, WebAssembly) and returns `compute(input)`, `result`, `loading`, `error`, `ready`.
36
+ - **`useWorkerNotifications`** — Listens to a Worker's messages and maintains state: running tasks, completed/failed counts, event history, average task duration, throughput per second, and queue size. Worker posts `task_start` / `task_end` / `task_fail` / `queue_size`; returns `progress` (default view of all active worker data) plus individual stats.
36
37
  - Fully TypeScript compatible
37
38
  - Bundled with Microbundle
38
- - Zero dependencies (except `preact`)
39
+ - Zero dependencies (peer: `preact` or `react` — use `/react` for React)
39
40
 
40
41
  ---
41
42
 
@@ -45,22 +46,36 @@ A lightweight, extendable collection of React-like hooks for Preact, including u
45
46
  npm install preact-missing-hooks
46
47
  ```
47
48
 
49
+ Ensure your app has either **preact** or **react** installed (the package uses whichever is present).
50
+
48
51
  ---
49
52
 
50
53
  ## Import options
51
54
 
52
- - **Main package** Import from the package root:
53
- ```ts
54
- import { useThreadedWorker, useClipboard } from 'preact-missing-hooks'
55
- ```
55
+ Use the same import in Preact and React projects:
56
+
57
+ ```ts
58
+ import { useThreadedWorker, useClipboard } from "preact-missing-hooks";
59
+ ```
60
+
61
+ - **How it picks Preact vs React**
62
+ - **CommonJS / Node:** The package detects which of `preact` or `react` is installed and uses that build automatically.
63
+ - **ESM (Vite, Webpack, etc.):** Default is the Preact build. In a **React** app, add the `react` condition so the package resolves to the React build:
64
+ - **Vite:** `vite.config.ts` → `resolve: { conditions: ['react'] }`
65
+ - **Webpack:** `resolve.conditionNames` (or similar) to include `'react'`
66
+ - **Or** in React projects you can always import from the explicit entry: `preact-missing-hooks/react`.
67
+
56
68
  - **Subpath exports (tree-shakeable)** — Import a single hook:
69
+
57
70
  ```ts
58
- import { useThreadedWorker } from 'preact-missing-hooks/useThreadedWorker'
59
- import { useClipboard } from 'preact-missing-hooks/useClipboard'
60
- import { useWebRTCIP } from 'preact-missing-hooks/useWebRTCIP'
61
- import { useWasmCompute } from 'preact-missing-hooks/useWasmCompute'
71
+ import { useThreadedWorker } from "preact-missing-hooks/useThreadedWorker";
72
+ import { useClipboard } from "preact-missing-hooks/useClipboard";
73
+ import { useWebRTCIP } from "preact-missing-hooks/useWebRTCIP";
74
+ import { useWasmCompute } from "preact-missing-hooks/useWasmCompute";
75
+ import { useWorkerNotifications } from "preact-missing-hooks/useWorkerNotifications";
62
76
  ```
63
- All hooks are available: `useTransition`, `useMutationObserver`, `useEventBus`, `useWrappedChildren`, `usePreferredTheme`, `useNetworkState`, `useClipboard`, `useRageClick`, `useThreadedWorker`, `useIndexedDB`, `useWebRTCIP`, `useWasmCompute`.
77
+
78
+ All hooks are available: `useTransition`, `useMutationObserver`, `useEventBus`, `useWrappedChildren`, `usePreferredTheme`, `useNetworkState`, `useClipboard`, `useRageClick`, `useThreadedWorker`, `useIndexedDB`, `useWebRTCIP`, `useWasmCompute`, `useWorkerNotifications`.
64
79
 
65
80
  ---
66
81
 
@@ -69,22 +84,22 @@ npm install preact-missing-hooks
69
84
  ### `useTransition`
70
85
 
71
86
  ```tsx
72
- import { useTransition } from 'preact-missing-hooks'
87
+ import { useTransition } from "preact-missing-hooks";
73
88
 
74
89
  function ExampleTransition() {
75
- const [startTransition, isPending] = useTransition()
90
+ const [startTransition, isPending] = useTransition();
76
91
 
77
92
  const handleClick = () => {
78
93
  startTransition(() => {
79
94
  // perform an expensive update here
80
- })
81
- }
95
+ });
96
+ };
82
97
 
83
98
  return (
84
99
  <button onClick={handleClick} disabled={isPending}>
85
- {isPending ? 'Loading...' : 'Click Me'}
100
+ {isPending ? "Loading..." : "Click Me"}
86
101
  </button>
87
- )
102
+ );
88
103
  }
89
104
  ```
90
105
 
@@ -93,21 +108,21 @@ function ExampleTransition() {
93
108
  ### `useMutationObserver`
94
109
 
95
110
  ```tsx
96
- import { useRef } from 'preact/hooks'
97
- import { useMutationObserver } from 'preact-missing-hooks'
111
+ import { useRef } from "preact/hooks";
112
+ import { useMutationObserver } from "preact-missing-hooks";
98
113
 
99
114
  function ExampleMutation() {
100
- const ref = useRef<HTMLDivElement>(null)
115
+ const ref = useRef<HTMLDivElement>(null);
101
116
 
102
117
  useMutationObserver(
103
118
  ref,
104
119
  (mutations) => {
105
- console.log('Detected mutations:', mutations)
120
+ console.log("Detected mutations:", mutations);
106
121
  },
107
- { childList: true, subtree: true },
108
- )
122
+ { childList: true, subtree: true }
123
+ );
109
124
 
110
- return <div ref={ref}>Observe this content</div>
125
+ return <div ref={ref}>Observe this content</div>;
111
126
  }
112
127
  ```
113
128
 
@@ -118,33 +133,33 @@ function ExampleMutation() {
118
133
  ```tsx
119
134
  // types.ts
120
135
  export type Events = {
121
- notify: (message: string) => void
122
- }
136
+ notify: (message: string) => void;
137
+ };
123
138
 
124
139
  // Sender.tsx
125
- import { useEventBus } from 'preact-missing-hooks'
126
- import type { Events } from './types'
140
+ import { useEventBus } from "preact-missing-hooks";
141
+ import type { Events } from "./types";
127
142
 
128
143
  function Sender() {
129
- const { emit } = useEventBus<Events>()
130
- return <button onClick={() => emit('notify', 'Hello World!')}>Send</button>
144
+ const { emit } = useEventBus<Events>();
145
+ return <button onClick={() => emit("notify", "Hello World!")}>Send</button>;
131
146
  }
132
147
 
133
148
  // Receiver.tsx
134
- import { useEventBus } from 'preact-missing-hooks'
135
- import { useState, useEffect } from 'preact/hooks'
136
- import type { Events } from './types'
149
+ import { useEventBus } from "preact-missing-hooks";
150
+ import { useState, useEffect } from "preact/hooks";
151
+ import type { Events } from "./types";
137
152
 
138
153
  function Receiver() {
139
- const [msg, setMsg] = useState<string>('')
140
- const { on } = useEventBus<Events>()
154
+ const [msg, setMsg] = useState<string>("");
155
+ const { on } = useEventBus<Events>();
141
156
 
142
157
  useEffect(() => {
143
- const unsubscribe = on('notify', setMsg)
144
- return unsubscribe
145
- }, [])
158
+ const unsubscribe = on("notify", setMsg);
159
+ return unsubscribe;
160
+ }, []);
146
161
 
147
- return <div>Message: {msg}</div>
162
+ return <div>Message: {msg}</div>;
148
163
  }
149
164
  ```
150
165
 
@@ -153,19 +168,19 @@ function Receiver() {
153
168
  ### `useWrappedChildren`
154
169
 
155
170
  ```tsx
156
- import { useWrappedChildren } from 'preact-missing-hooks'
171
+ import { useWrappedChildren } from "preact-missing-hooks";
157
172
 
158
173
  function ParentComponent({ children }) {
159
174
  // Inject common props into all children
160
175
  const injectProps = {
161
- className: 'enhanced-child',
162
- onClick: () => console.log('Child clicked!'),
163
- style: { border: '1px solid #ccc' },
164
- }
176
+ className: "enhanced-child",
177
+ onClick: () => console.log("Child clicked!"),
178
+ style: { border: "1px solid #ccc" },
179
+ };
165
180
 
166
- const wrappedChildren = useWrappedChildren(children, injectProps)
181
+ const wrappedChildren = useWrappedChildren(children, injectProps);
167
182
 
168
- return <div className="parent">{wrappedChildren}</div>
183
+ return <div className="parent">{wrappedChildren}</div>;
169
184
  }
170
185
 
171
186
  // Usage with preserve strategy (default - existing props are preserved)
@@ -173,21 +188,21 @@ function PreserveExample() {
173
188
  return (
174
189
  <ParentComponent>
175
190
  <button className="btn">Existing class preserved</button>
176
- <span style={{ color: 'red' }}>Both styles applied</span>
191
+ <span style={{ color: "red" }}>Both styles applied</span>
177
192
  </ParentComponent>
178
- )
193
+ );
179
194
  }
180
195
 
181
196
  // Usage with override strategy (injected props override existing ones)
182
197
  function OverrideExample() {
183
- const injectProps = { className: 'new-class' }
198
+ const injectProps = { className: "new-class" };
184
199
  const children = (
185
200
  <button className="old-class">Class will be overridden</button>
186
- )
201
+ );
187
202
 
188
- const wrappedChildren = useWrappedChildren(children, injectProps, 'override')
203
+ const wrappedChildren = useWrappedChildren(children, injectProps, "override");
189
204
 
190
- return <div>{wrappedChildren}</div>
205
+ return <div>{wrappedChildren}</div>;
191
206
  }
192
207
  ```
193
208
 
@@ -196,12 +211,12 @@ function OverrideExample() {
196
211
  ### `usePreferredTheme`
197
212
 
198
213
  ```tsx
199
- import { usePreferredTheme } from 'preact-missing-hooks'
214
+ import { usePreferredTheme } from "preact-missing-hooks";
200
215
 
201
216
  function ThemeAwareComponent() {
202
- const theme = usePreferredTheme() // 'light' | 'dark' | 'no-preference'
217
+ const theme = usePreferredTheme(); // 'light' | 'dark' | 'no-preference'
203
218
 
204
- return <div data-theme={theme}>Your system prefers: {theme}</div>
219
+ return <div data-theme={theme}>Your system prefers: {theme}</div>;
205
220
  }
206
221
  ```
207
222
 
@@ -210,18 +225,18 @@ function ThemeAwareComponent() {
210
225
  ### `useNetworkState`
211
226
 
212
227
  ```tsx
213
- import { useNetworkState } from 'preact-missing-hooks'
228
+ import { useNetworkState } from "preact-missing-hooks";
214
229
 
215
230
  function NetworkStatus() {
216
- const { online, effectiveType, saveData } = useNetworkState()
231
+ const { online, effectiveType, saveData } = useNetworkState();
217
232
 
218
233
  return (
219
234
  <div>
220
- Status: {online ? 'Online' : 'Offline'}
235
+ Status: {online ? "Online" : "Offline"}
221
236
  {effectiveType && ` (${effectiveType})`}
222
- {saveData && ' — Reduced data mode enabled'}
237
+ {saveData && " — Reduced data mode enabled"}
223
238
  </div>
224
- )
239
+ );
225
240
  }
226
241
  ```
227
242
 
@@ -230,34 +245,34 @@ function NetworkStatus() {
230
245
  ### `useClipboard`
231
246
 
232
247
  ```tsx
233
- import { useState } from 'preact/hooks'
234
- import { useClipboard } from 'preact-missing-hooks'
248
+ import { useState } from "preact/hooks";
249
+ import { useClipboard } from "preact-missing-hooks";
235
250
 
236
251
  function CopyButton() {
237
- const { copy, copied, error } = useClipboard({ resetDelay: 2000 })
252
+ const { copy, copied, error } = useClipboard({ resetDelay: 2000 });
238
253
 
239
254
  return (
240
- <button onClick={() => copy('Hello, World!')}>
241
- {copied ? 'Copied!' : 'Copy'}
255
+ <button onClick={() => copy("Hello, World!")}>
256
+ {copied ? "Copied!" : "Copy"}
242
257
  </button>
243
- )
258
+ );
244
259
  }
245
260
 
246
261
  function PasteInput() {
247
- const [text, setText] = useState('')
248
- const { paste } = useClipboard()
262
+ const [text, setText] = useState("");
263
+ const { paste } = useClipboard();
249
264
 
250
265
  const handlePaste = async () => {
251
- const content = await paste()
252
- setText(content)
253
- }
266
+ const content = await paste();
267
+ setText(content);
268
+ };
254
269
 
255
270
  return (
256
271
  <div>
257
272
  <input value={text} onChange={(e) => setText(e.target.value)} />
258
273
  <button onClick={handlePaste}>Paste</button>
259
274
  </div>
260
- )
275
+ );
261
276
  }
262
277
  ```
263
278
 
@@ -268,26 +283,26 @@ function PasteInput() {
268
283
  Detects rage clicks (multiple rapid clicks in the same area), e.g. when the UI is unresponsive. Report them to [Sentry](https://docs.sentry.io/product/issues/issue-details/replay-issues/rage-clicks/) or your error tracker to surface rage-click issues and lower rage-click-related support.
269
284
 
270
285
  ```tsx
271
- import { useRef } from 'preact/hooks'
272
- import { useRageClick } from 'preact-missing-hooks'
286
+ import { useRef } from "preact/hooks";
287
+ import { useRageClick } from "preact-missing-hooks";
273
288
 
274
289
  function SubmitButton() {
275
- const ref = useRef<HTMLButtonElement>(null)
290
+ const ref = useRef<HTMLButtonElement>(null);
276
291
 
277
292
  useRageClick(ref, {
278
293
  onRageClick: ({ count, event }) => {
279
294
  // Report to Sentry (or your error tracker) to create rage-click issues
280
- Sentry.captureMessage('Rage click detected', {
281
- level: 'warning',
282
- extra: { count, target: event.target, tag: 'rage_click' },
283
- })
295
+ Sentry.captureMessage("Rage click detected", {
296
+ level: "warning",
297
+ extra: { count, target: event.target, tag: "rage_click" },
298
+ });
284
299
  },
285
300
  threshold: 5, // min clicks (default 5, Sentry-style)
286
301
  timeWindow: 1000, // ms (default 1000)
287
302
  distanceThreshold: 30, // px (default 30)
288
- })
303
+ });
289
304
 
290
- return <button ref={ref}>Submit</button>
305
+ return <button ref={ref}>Submit</button>;
291
306
  }
292
307
  ```
293
308
 
@@ -298,16 +313,16 @@ function SubmitButton() {
298
313
  Runs async work in a queue with **sequential** (one task at a time, by priority) or **parallel** (worker pool) execution. Lower priority number = higher priority; same priority is FIFO.
299
314
 
300
315
  ```tsx
301
- import { useThreadedWorker } from 'preact-missing-hooks'
316
+ import { useThreadedWorker } from "preact-missing-hooks";
302
317
 
303
318
  // Sequential: one task at a time, sorted by priority
304
- const sequential = useThreadedWorker(fetchUser, { mode: 'sequential' })
319
+ const sequential = useThreadedWorker(fetchUser, { mode: "sequential" });
305
320
 
306
321
  // Parallel: up to N tasks at once
307
322
  const parallel = useThreadedWorker(processItem, {
308
- mode: 'parallel',
323
+ mode: "parallel",
309
324
  concurrency: 4,
310
- })
325
+ });
311
326
 
312
327
  // API (same for both modes)
313
328
  const {
@@ -318,11 +333,11 @@ const {
318
333
  queueSize, // tasks queued + running
319
334
  clearQueue, // clear pending tasks (running continue)
320
335
  terminate, // clear queue and reject new run()
321
- } = sequential
336
+ } = sequential;
322
337
 
323
338
  // Run with priority (1 = highest)
324
- await run({ userId: 1 }, { priority: 1 })
325
- await run({ userId: 2 }, { priority: 3 })
339
+ await run({ userId: 1 }, { priority: 1 });
340
+ await run({ userId: 2 }, { priority: 3 });
326
341
  ```
327
342
 
328
343
  ---
@@ -338,40 +353,40 @@ Production-ready IndexedDB hook: database initialization, table creation (with k
338
353
  **Database API:** `db.table(name)`, `db.hasTable(name)`, `db.transaction(storeNames, mode, callback, options?)`.
339
354
 
340
355
  ```tsx
341
- import { useIndexedDB } from 'preact-missing-hooks'
356
+ import { useIndexedDB } from "preact-missing-hooks";
342
357
 
343
358
  function App() {
344
359
  const { db, isReady, error } = useIndexedDB({
345
- name: 'my-app-db',
360
+ name: "my-app-db",
346
361
  version: 1,
347
362
  tables: {
348
- users: { keyPath: 'id', autoIncrement: true, indexes: ['email'] },
349
- settings: { keyPath: 'key' },
363
+ users: { keyPath: "id", autoIncrement: true, indexes: ["email"] },
364
+ settings: { keyPath: "key" },
350
365
  },
351
- })
366
+ });
352
367
 
353
- if (error) return <div>Failed to open database</div>
354
- if (!isReady || !db) return <div>Loading...</div>
368
+ if (error) return <div>Failed to open database</div>;
369
+ if (!isReady || !db) return <div>Loading...</div>;
355
370
 
356
- const users = db.table('users')
371
+ const users = db.table("users");
357
372
 
358
373
  // All operations return Promises and accept optional { onSuccess, onError }
359
- await users.insert({ email: 'a@b.com', name: 'Alice' })
360
- await users.update(1, { name: 'Alice Smith' })
361
- const found = await users.query((u) => u.email.startsWith('a@'))
362
- const n = await users.count()
363
- await users.delete(1)
364
- await users.upsert({ id: 2, email: 'b@b.com' })
365
- await users.bulkInsert([{ email: 'c@b.com' }, { email: 'd@b.com' }])
366
- await users.clear()
374
+ await users.insert({ email: "a@b.com", name: "Alice" });
375
+ await users.update(1, { name: "Alice Smith" });
376
+ const found = await users.query((u) => u.email.startsWith("a@"));
377
+ const n = await users.count();
378
+ await users.delete(1);
379
+ await users.upsert({ id: 2, email: "b@b.com" });
380
+ await users.bulkInsert([{ email: "c@b.com" }, { email: "d@b.com" }]);
381
+ await users.clear();
367
382
 
368
383
  // Full transaction support
369
- await db.transaction(['users', 'settings'], 'readwrite', async (tx) => {
370
- await tx.table('users').insert({ email: 'e@b.com' })
371
- await tx.table('settings').upsert({ key: 'theme', value: 'dark' })
372
- })
384
+ await db.transaction(["users", "settings"], "readwrite", async (tx) => {
385
+ await tx.table("users").insert({ email: "e@b.com" });
386
+ await tx.table("settings").upsert({ key: "theme", value: "dark" });
387
+ });
373
388
 
374
- return <div>DB ready. Tables: {db.hasTable('users') ? 'users' : ''}</div>
389
+ return <div>DB ready. Tables: {db.hasTable("users") ? "users" : ""}</div>;
375
390
  }
376
391
  ```
377
392
 
@@ -384,8 +399,8 @@ Detects client IP addresses using WebRTC ICE candidates and a STUN server (**fro
384
399
  Returns `{ ips: string[], loading: boolean, error: string | null }`. Options: `stunServers`, `timeout` (ms), `onDetect(ip)`.
385
400
 
386
401
  ```tsx
387
- import { useWebRTCIP } from 'preact-missing-hooks'
388
- import { useState, useEffect } from 'preact/hooks'
402
+ import { useWebRTCIP } from "preact-missing-hooks";
403
+ import { useState, useEffect } from "preact/hooks";
389
404
 
390
405
  function ClientIP() {
391
406
  const { ips, loading, error } = useWebRTCIP({
@@ -393,25 +408,25 @@ function ClientIP() {
393
408
  onDetect: (ip) => {
394
409
  /* optional: e.g. analytics */
395
410
  },
396
- })
397
- const [fallbackIP, setFallbackIP] = useState<string | null>(null)
411
+ });
412
+ const [fallbackIP, setFallbackIP] = useState<string | null>(null);
398
413
 
399
414
  // Fallback to public IP API when WebRTC fails or returns empty
400
415
  useEffect(() => {
401
- if (loading || ips.length > 0) return
416
+ if (loading || ips.length > 0) return;
402
417
  if (error) {
403
- fetch('https://api.ipify.org?format=json')
418
+ fetch("https://api.ipify.org?format=json")
404
419
  .then((r) => r.json())
405
420
  .then((d) => setFallbackIP(d.ip))
406
- .catch(() => {})
421
+ .catch(() => {});
407
422
  }
408
- }, [loading, ips.length, error])
423
+ }, [loading, ips.length, error]);
409
424
 
410
- if (loading) return <p>Detecting IP…</p>
411
- if (ips.length > 0) return <p>IPs (WebRTC): {ips.join(', ')}</p>
412
- if (fallbackIP) return <p>IP (fallback API): {fallbackIP}</p>
413
- if (error) return <p>WebRTC failed. Try fallback API.</p>
414
- return null
425
+ if (loading) return <p>Detecting IP…</p>;
426
+ if (ips.length > 0) return <p>IPs (WebRTC): {ips.join(", ")}</p>;
427
+ if (fallbackIP) return <p>IP (fallback API): {fallbackIP}</p>;
428
+ if (error) return <p>WebRTC failed. Try fallback API.</p>;
429
+ return null;
415
430
  }
416
431
  ```
417
432
 
@@ -424,23 +439,23 @@ Runs WebAssembly computation in a Web Worker so the main thread stays responsive
424
439
  Returns `{ compute, result, loading, error, ready }`. Options: `wasmUrl` (required), `exportName` (default `'compute'`), optional `workerUrl` (custom worker script), optional `importObject` (must be serializable for the default worker).
425
440
 
426
441
  ```tsx
427
- import { useWasmCompute } from 'preact-missing-hooks'
442
+ import { useWasmCompute } from "preact-missing-hooks";
428
443
 
429
444
  function AddWithWasm() {
430
445
  const { compute, result, loading, error, ready } = useWasmCompute<
431
446
  number,
432
447
  number
433
448
  >({
434
- wasmUrl: '/add.wasm',
435
- exportName: 'add',
436
- })
449
+ wasmUrl: "/add.wasm",
450
+ exportName: "add",
451
+ });
437
452
 
438
453
  const handleClick = () => {
439
- if (ready) compute(2).then(() => {})
440
- }
454
+ if (ready) compute(2).then(() => {});
455
+ };
441
456
 
442
- if (error) return <p>WASM unavailable: {error}</p>
443
- if (!ready) return <p>Loading WASM…</p>
457
+ if (error) return <p>WASM unavailable: {error}</p>;
458
+ if (!ready) return <p>Loading WASM…</p>;
444
459
  return (
445
460
  <div>
446
461
  <button onClick={handleClick} disabled={loading}>
@@ -448,7 +463,40 @@ function AddWithWasm() {
448
463
  </button>
449
464
  {result != null && <p>Result: {result}</p>}
450
465
  </div>
451
- )
466
+ );
467
+ }
468
+ ```
469
+
470
+ ---
471
+
472
+ ### `useWorkerNotifications`
473
+
474
+ Listens to a Worker's `message` events and maintains state and derived stats. Your worker should `postMessage` with: `{ type: 'task_start', taskId? }`, `{ type: 'task_end', taskId?, duration? }`, `{ type: 'task_fail', taskId?, error? }`, and optionally `{ type: 'queue_size', size }`.
475
+
476
+ Returns `runningTasks`, `completedCount`, `failedCount`, `eventHistory`, `averageDurationMs`, `throughputPerSecond`, `currentQueueSize`, and **`progress`** — a single object with all active worker data (running, completed, failed, totalProcessed, avg duration, throughput/s, queue). Options: `maxHistory` (default 100), `throughputWindowMs` (default 1000).
477
+
478
+ ```tsx
479
+ import { useWorkerNotifications } from "preact-missing-hooks";
480
+
481
+ function WorkerDashboard({ worker }) {
482
+ const { progress, eventHistory } = useWorkerNotifications(worker, {
483
+ maxHistory: 50,
484
+ });
485
+
486
+ return (
487
+ <div>
488
+ <p>
489
+ Running: {progress.runningTasks.length} | Done:{" "}
490
+ {progress.completedCount} | Failed: {progress.failedCount}
491
+ </p>
492
+ <p>
493
+ Avg: {progress.averageDurationMs.toFixed(0)}ms | Throughput:{" "}
494
+ {progress.throughputPerSecond.toFixed(2)}/s | Queue:{" "}
495
+ {progress.currentQueueSize}
496
+ </p>
497
+ <small>Events: {eventHistory.length}</small>
498
+ </div>
499
+ );
452
500
  }
453
501
  ```
454
502
 
package/dist/entry.cjs ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /**
3
+ * Auto-detect Preact vs React and re-export the matching build.
4
+ * Used for require('preact-missing-hooks') in Node / CJS bundlers.
5
+ */
6
+ function detect() {
7
+ try {
8
+ require.resolve("preact");
9
+ return require("./index.js");
10
+ } catch (_) {
11
+ try {
12
+ require.resolve("react");
13
+ return require("./react.js");
14
+ } catch (_) {
15
+ throw new Error(
16
+ "preact-missing-hooks: Install either \"preact\" or \"react\" in your project."
17
+ );
18
+ }
19
+ }
20
+ }
21
+ module.exports = detect();
package/dist/entry.js ADDED
@@ -0,0 +1,2 @@
1
+ var e=require("preact/hooks"),n=require("preact"),r=new Map;function t(e,n){(null==n||n>e.length)&&(n=e.length);for(var r=0,t=Array(n);r<n;r++)t[r]=e[r];return t}function o(e,n){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(r)return(r=r.call(e)).next.bind(r);if(Array.isArray(e)||(r=function(e,n){if(e){if("string"==typeof e)return t(e,n);var r={}.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?t(e,n):void 0}}(e))||n&&e&&"number"==typeof e.length){r&&(e=r);var o=0;return function(){return o>=e.length?{done:!0}:{done:!1,value:e[o++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function u(){return u=Object.assign?Object.assign.bind():function(e){for(var n=1;n<arguments.length;n++){var r=arguments[n];for(var t in r)({}).hasOwnProperty.call(r,t)&&(e[t]=r[t])}return e},u.apply(null,arguments)}function i(){if("undefined"==typeof navigator)return{online:!0};var e={online:navigator.onLine},n=navigator.connection;return n&&(void 0!==n.effectiveType&&(e.effectiveType=n.effectiveType),void 0!==n.downlink&&(e.downlink=n.downlink),void 0!==n.rtt&&(e.rtt=n.rtt),void 0!==n.saveData&&(e.saveData=n.saveData),void 0!==n.type&&(e.connectionType=n.type)),e}function a(e,n){try{var r=e()}catch(e){return n(e)}return r&&r.then?r.then(void 0,n):r}var c=new Map;function s(e){return new Promise(function(n,r){e.onsuccess=function(){return n(e.result)},e.onerror=function(){var n;return r(null!=(n=e.error)?n:new DOMException("Unknown IndexedDB error"))}})}function f(e,n){return n?e.then(function(e){return null==n.onSuccess||n.onSuccess(e),e}).catch(function(e){throw null==n.onError||n.onError(e),e}):e}var l=/\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,d=["stun:stun.l.google.com:19302"];exports.useClipboard=function(n){void 0===n&&(n={});var r=n.resetDelay,t=void 0===r?2e3:r,o=e.useState(!1),u=o[0],i=o[1],c=e.useState(null),s=c[0],f=c[1],l=e.useCallback(function(){i(!1),f(null)},[]);return{copy:e.useCallback(function(e){try{if(f(null),"undefined"==typeof navigator||!navigator.clipboard){var n=new Error("Clipboard API is not available");return f(n),Promise.resolve(!1)}return Promise.resolve(a(function(){return Promise.resolve(navigator.clipboard.writeText(e)).then(function(){return i(!0),t>0&&setTimeout(function(){return i(!1)},t),!0})},function(e){var n=e instanceof Error?e:new Error(String(e));return f(n),!1}))}catch(e){return Promise.reject(e)}},[t]),paste:e.useCallback(function(){try{if(f(null),"undefined"==typeof navigator||!navigator.clipboard){var e=new Error("Clipboard API is not available");return f(e),Promise.resolve("")}return Promise.resolve(a(function(){return Promise.resolve(navigator.clipboard.readText())},function(e){var n=e instanceof Error?e:new Error(String(e));return f(n),""}))}catch(e){return Promise.reject(e)}},[]),copied:u,error:s,reset:l}},exports.useEventBus=function(){return{emit:e.useCallback(function(e){var n=arguments,t=r.get(e);t&&t.forEach(function(e){return e.apply(void 0,[].slice.call(n,1))})},[]),on:e.useCallback(function(e,n){var t=r.get(e);return t||(t=new Set,r.set(e,t)),t.add(n),function(){t.delete(n),0===t.size&&r.delete(e)}},[])}},exports.useIndexedDB=function(n){var r=e.useState(null),t=r[0],i=r[1],a=e.useState(null),l=a[0],d=a[1],v=e.useState(!1),p=v[0],m=v[1],h=e.useRef(n);return h.current=n,e.useEffect(function(){var e=!1;d(null),m(!1),i(null);var n=h.current;return function(e){var n=e.name+"_v"+e.version,r=c.get(n);return r||(r=function(e){return new Promise(function(n,r){var t=indexedDB.open(e.name,e.version);t.onerror=function(){var e;return r(null!=(e=t.error)?e:new DOMException("Failed to open database"))},t.onsuccess=function(){return n(t.result)},t.onupgradeneeded=function(n){for(var r=n.target.result,t=e.tables,u=0,i=Object.keys(t);u<i.length;u++){var a=i[u],c=t[a];if(!r.objectStoreNames.contains(a)){var s,f=r.createObjectStore(a,{keyPath:c.keyPath,autoIncrement:null!=(s=c.autoIncrement)&&s});if(c.indexes)for(var l,d=o(c.indexes);!(l=d()).done;){var v=l.value;f.createIndex(v,v,{unique:!1})}}}}})}(e),c.set(n,r),r)}({name:n.name,version:n.version,tables:n.tables}).then(function(n){if(e)n.close();else{var r=function(e){return{get db(){return e},hasTable:function(n){return e.objectStoreNames.contains(n)},table:function(n){return function(e,n){return function(e,n){function r(r){return e.transaction([n],r).objectStore(n)}return{insert:function(e,n){return f(s(r("readwrite").add(e)),n)},update:function(e,n,t){var o=r("readwrite");return f(s(o.get(e)).then(function(e){if(void 0===e)throw new DOMException("Key not found","NotFoundError");var r=u({},e,n);return s(o.put(r))}).then(function(){}),t)},delete:function(e,n){return f(s(r("readwrite").delete(e)).then(function(){}),n)},exists:function(e){return s(r("readonly").getKey(e)).then(function(e){return void 0!==e})},query:function(e,n){var t=r("readonly").openCursor(),o=[];return f(new Promise(function(n,r){t.onsuccess=function(){var r=t.result;r?(e(r.value)&&o.push(r.value),r.continue()):n(o)},t.onerror=function(){var e;return r(null!=(e=t.error)?e:new DOMException("Unknown error"))}}),n)},upsert:function(e,n){return f(s(r("readwrite").put(e)),n)},bulkInsert:function(e,n){var t=r("readwrite"),o=[];if(0===e.length)return f(Promise.resolve(o),n);var u=0;return f(new Promise(function(n,r){e.forEach(function(i,a){var c=t.add(i);c.onsuccess=function(){o[a]=c.result,++u===e.length&&n(o)},c.onerror=function(){var e;return r(null!=(e=c.error)?e:new DOMException("Unknown error"))}})}),n)},clear:function(e){return f(s(r("readwrite").clear()).then(function(){}),e)},count:function(e){return f(s(r("readonly").count()),null!=e?e:{})}}}(e,n)}(e,n)},transaction:function(n,r,t,o){var i=e.transaction(n,r),a={table:function(e){return function(e,n){return function(e,n){function r(){return e.objectStore(n)}return{insert:function(e,n){return f(s(r().add(e)),n)},update:function(e,n,t){var o=r();return f(s(o.get(e)).then(function(e){if(void 0===e)throw new DOMException("Key not found","NotFoundError");var r=u({},e,n);return s(o.put(r))}).then(function(){}),t)},delete:function(e,n){return f(s(r().delete(e)).then(function(){}),n)},exists:function(e){return s(r().getKey(e)).then(function(e){return void 0!==e})},query:function(e,n){var t=r().openCursor(),o=[];return f(new Promise(function(n,r){t.onsuccess=function(){var r=t.result;r?(e(r.value)&&o.push(r.value),r.continue()):n(o)},t.onerror=function(){var e;return r(null!=(e=t.error)?e:new DOMException("Unknown error"))}}),n)},upsert:function(e,n){return f(s(r().put(e)),n)},bulkInsert:function(e,n){var t=r(),o=[];if(0===e.length)return f(Promise.resolve(o),n);var u=0;return f(new Promise(function(n,r){e.forEach(function(i,a){var c=t.add(i);c.onsuccess=function(){o[a]=c.result,++u===e.length&&n(o)},c.onerror=function(){var e;return r(null!=(e=c.error)?e:new DOMException("Unknown error"))}})}),n)},clear:function(e){return f(s(r().clear()).then(function(){}),e)},count:function(e){return f(s(r().count()),null!=e?e:{})}}}(e,n)}(i,e)}},c=new Promise(function(e,n){i.oncomplete=function(){return e()},i.onerror=function(){var e;return n(null!=(e=i.error)?e:new DOMException("Transaction failed"))}}),l=t(a);return function(e,n){return n?e.then(function(){return null==n.onSuccess?void 0:n.onSuccess()}).catch(function(e){throw null==n.onError||n.onError(e),e}):e}(Promise.resolve(l).then(function(){return c}),o)}}}(n);i(r),m(!0)}}).catch(function(n){e||d(n)}),function(){e=!0}},[n.name,n.version]),{db:t,isReady:p,error:l}},exports.useMutationObserver=function(n,r,t){e.useEffect(function(){var e=n.current;if(e){var o=new MutationObserver(r);return o.observe(e,t),function(){return o.disconnect()}}},[n,r,t])},exports.useNetworkState=function(){var n=e.useState(i),r=n[0],t=n[1];return e.useEffect(function(){if("undefined"!=typeof window){var e=function(){return t(i())};window.addEventListener("online",e),window.addEventListener("offline",e);var n=navigator.connection;return null!=n&&n.addEventListener&&n.addEventListener("change",e),function(){window.removeEventListener("online",e),window.removeEventListener("offline",e),null!=n&&n.removeEventListener&&n.removeEventListener("change",e)}}},[]),r},exports.usePreferredTheme=function(){var n=e.useState(function(){if("undefined"==typeof window)return"no-preference";var e=window.matchMedia("(prefers-color-scheme: dark)"),n=window.matchMedia("(prefers-color-scheme: light)");return e.matches?"dark":n.matches?"light":"no-preference"}),r=n[0],t=n[1];return e.useEffect(function(){if("undefined"!=typeof window){var e=window.matchMedia("(prefers-color-scheme: dark)"),n=function(e){t(e.matches?"dark":"light")},r=function(){var e=window.matchMedia("(prefers-color-scheme: dark)"),n=window.matchMedia("(prefers-color-scheme: light)");t(e.matches?"dark":n.matches?"light":"no-preference")};e.addEventListener("change",n);var o=window.matchMedia("(prefers-color-scheme: light)");return o.addEventListener("change",r),function(){e.removeEventListener("change",n),o.removeEventListener("change",r)}}},[]),r},exports.useRageClick=function(n,r){var t=r.onRageClick,o=r.threshold,u=void 0===o?5:o,i=r.timeWindow,a=void 0===i?1e3:i,c=r.distanceThreshold,s=void 0===c?30:c,f=e.useRef(t);f.current=t;var l=e.useRef([]);e.useEffect(function(){var e=n.current;if(e){var r=function(e){var n=Date.now(),r={time:n,x:e.clientX,y:e.clientY},t=n-a,o=l.current.filter(function(e){return e.time>=t});if(o.push(r),Infinity!==s){var i=o.filter(function(e){return n=e,t=r,Math.hypot(t.x-n.x,t.y-n.y)<=s;var n,t});if(i.length>=u)return f.current({count:i.length,event:e}),void(l.current=[])}else if(o.length>=u)return f.current({count:o.length,event:e}),void(l.current=[]);l.current=o};return e.addEventListener("click",r),function(){return e.removeEventListener("click",r)}}},[n,u,a,s])},exports.useThreadedWorker=function(n,r){var t=r.concurrency,o="sequential"===r.mode?1:Math.max(1,void 0===t?4:t),u=e.useState(!1),i=u[0],a=u[1],c=e.useState(void 0),s=c[0],f=c[1],l=e.useState(void 0),d=l[0],v=l[1],p=e.useState(0),m=p[0],h=p[1],y=e.useRef([]),w=e.useRef(0),g=e.useRef(0),b=e.useRef(!1),k=e.useRef(n);k.current=n;var E=e.useCallback(function(){h(y.current.length+g.current)},[]),S=e.useCallback(function(){if(!(b.current||g.current>=o)){if(0===y.current.length)return 0===g.current&&a(!1),void E();y.current.sort(function(e,n){return e.priority!==n.priority?e.priority-n.priority:e.sequence-n.sequence});var e=y.current.shift();g.current+=1,a(!0),E(),(0,k.current)(e.data).then(function(n){f(n),v(void 0),e.resolve(n)}).catch(function(n){v(n),e.reject(n)}).finally(function(){g.current-=1,E(),S()}),y.current.length>0&&g.current<o&&S()}},[o,E]),x=e.useCallback(function(e,n){var r;if(b.current)return Promise.reject(new Error("Worker is terminated"));var t=null!=(r=null==n?void 0:n.priority)?r:1,o=++w.current,u=new Promise(function(n,r){y.current.push({data:e,priority:t,sequence:o,resolve:n,reject:r})});return E(),a(!0),queueMicrotask(S),u},[S,E]),C=e.useCallback(function(){var e=y.current;y.current=[],e.forEach(function(e){return e.reject(new Error("Task cleared from queue"))}),E(),0===g.current&&a(!1)},[E]),M=e.useCallback(function(){b.current=!0,C()},[C]);return e.useEffect(function(){return function(){b.current=!0}},[]),{run:x,loading:i,result:s,error:d,queueSize:m,clearQueue:C,terminate:M}},exports.useTransition=function(){var n=e.useState(!1),r=n[0],t=n[1];return[e.useCallback(function(e){t(!0),Promise.resolve().then(function(){e(),t(!1)})},[]),r]},exports.useWasmCompute=function(n){var r=n.wasmUrl,t=n.exportName,o=void 0===t?"compute":t,u=n.workerUrl,i=n.importObject,a=e.useState(void 0),c=a[0],s=a[1],f=e.useState(!0),l=f[0],d=f[1],v=e.useState(null),p=v[0],m=v[1],h=e.useState(!1),y=h[0],w=h[1],g=e.useRef(null),b=e.useRef(null),k=e.useRef(null);return e.useEffect(function(){if("undefined"==typeof window)return m("useWasmCompute is not available during SSR"),void d(!1);if("undefined"==typeof Worker)return m("Worker is not supported in this environment"),void d(!1);if("undefined"==typeof WebAssembly||"function"!=typeof WebAssembly.instantiate)return m("WebAssembly is not supported in this environment"),void d(!1);m(null),w(!1);var e=function(e){if(e)return new Worker(e);var n=new Blob(["\nself.onmessage = async (e) => {\n const d = e.data;\n if (d.type === 'init') {\n try {\n const res = await fetch(d.wasmUrl);\n const buf = await res.arrayBuffer();\n const mod = await WebAssembly.instantiate(buf, d.importObject || {});\n self.wasmInstance = mod.instance;\n self.exportName = d.exportName || 'compute';\n self.postMessage({ type: 'ready' });\n } catch (err) {\n self.postMessage({ type: 'error', error: (err && err.message) || String(err) });\n }\n return;\n }\n if (d.type === 'compute') {\n try {\n const fn = self.wasmInstance.exports[self.exportName];\n if (typeof fn !== 'function') {\n self.postMessage({ type: 'error', error: 'Export \"' + self.exportName + '\" is not a function' });\n return;\n }\n const result = fn(d.input);\n self.postMessage({ type: 'result', result: result });\n } catch (err) {\n self.postMessage({ type: 'error', error: (err && err.message) || String(err) });\n }\n }\n};\n"],{type:"application/javascript"}),r=URL.createObjectURL(n),t=new Worker(r);return URL.revokeObjectURL(r),t}(u);g.current=e;var n=function(e){var n,r=null!=(n=e.data)?n:{},t=r.type,o=r.result,u=r.error;return"ready"===t?(w(!0),void d(!1)):"error"===t?(m(null!=u?u:"Unknown error"),d(!1),void(k.current&&(k.current(new Error(u)),b.current=null,k.current=null))):void("result"===t&&(s(o),d(!1),b.current&&(b.current(o),b.current=null,k.current=null)))};return e.addEventListener("message",n),e.postMessage({type:"init",wasmUrl:r,exportName:o,importObject:null!=i?i:{}}),function(){e.removeEventListener("message",n),e.terminate(),g.current=null,k.current&&(k.current(new Error("Worker terminated")),b.current=null,k.current=null)}},[r,o,u,i]),{compute:e.useCallback(function(e){return new Promise(function(n,r){g.current&&y?p?r(new Error(p)):(b.current=n,k.current=r,d(!0),g.current.postMessage({type:"compute",input:e})):r(new Error("WASM not ready"))})},[y,p]),result:c,loading:l,error:p,ready:y}},exports.useWebRTCIP=function(n){void 0===n&&(n={});var r=n.stunServers,t=void 0===r?d:r,o=n.timeout,u=void 0===o?3e3:o,i=n.onDetect,a=e.useState([]),c=a[0],s=a[1],f=e.useState(!0),v=f[0],p=f[1],m=e.useState(null),h=m[0],y=m[1],w=e.useRef(null),g=e.useRef(null),b=e.useRef(new Set),k=e.useRef(i);return k.current=i,e.useEffect(function(){if("undefined"==typeof window)return p(!1),void y("WebRTC IP detection is not available during SSR");if("undefined"==typeof RTCPeerConnection)return p(!1),void y("RTCPeerConnection is not available");var e=new Set;b.current=e;var n=function(){g.current&&(clearTimeout(g.current),g.current=null),w.current&&(w.current.close(),w.current=null),p(!1)},r=function(n){e.has(n)||(e.add(n),s(function(e){return[].concat(e,[n])}),null==k.current||k.current(n))};try{var o=new RTCPeerConnection({iceServers:[{urls:t}]});w.current=o,o.onicecandidate=function(e){var n,t=e.candidate;t&&t.candidate&&((n=t.candidate.match(l))?[].concat(n):[]).forEach(r)},o.createDataChannel(""),o.createOffer().then(function(e){return o.setLocalDescription(e)}).catch(function(e){y(e instanceof Error?e.message:"Failed to create offer"),n()}),g.current=setTimeout(function(){return n()},u)}catch(e){y(e instanceof Error?e.message:"WebRTC setup failed"),n()}return function(){n()}},[t.join(","),u]),{ips:c,loading:v,error:h}},exports.useWorkerNotifications=function(n,r){void 0===r&&(r={});var t=r.maxHistory,o=void 0===t?100:t,u=r.throughputWindowMs,i=void 0===u?1e3:u,a=e.useState([]),c=a[0],s=a[1],f=e.useState(0),l=f[0],d=f[1],v=e.useState(0),p=v[0],m=v[1],h=e.useState([]),y=h[0],w=h[1],g=e.useState(0),b=g[0],k=g[1],E=e.useRef([]),S=e.useRef(0),x=e.useRef(0);e.useEffect(function(){if(n){var e=function(e){var n=function(e){if(null==e||"object"!=typeof e)return null;var n=e.type;return"task_start"!==n&&"task_end"!==n&&"task_fail"!==n&&"queue_size"!==n?null:{type:n,taskId:"string"==typeof e.taskId?e.taskId:void 0,duration:"number"==typeof e.duration?e.duration:void 0,error:"string"==typeof e.error?e.error:void 0,size:"number"==typeof e.size?e.size:void 0,timestamp:Date.now()}}(e.data);if(n)if(w(function(e){return[].concat(e,[n]).slice(-o)}),"task_start"===n.type&&n.taskId)s(function(e){return e.includes(n.taskId)?e:[].concat(e,[n.taskId])});else if("task_end"===n.type){n.taskId&&s(function(e){return e.filter(function(e){return e!==n.taskId})}),d(function(e){return e+1});var r=Date.now()-i;E.current=[].concat(E.current.filter(function(e){return e>=r}),[n.timestamp]),"number"==typeof n.duration&&(S.current+=n.duration,x.current+=1)}else"task_fail"===n.type?(n.taskId&&s(function(e){return e.filter(function(e){return e!==n.taskId})}),m(function(e){return e+1})):"queue_size"===n.type&&"number"==typeof n.size&&k(n.size)};return n.addEventListener("message",e),function(){return n.removeEventListener("message",e)}}},[n,o]);var C=e.useMemo(function(){var e=x.current;return e>0?S.current/e:0},[y]),M=e.useMemo(function(){var e=Date.now()-i;return E.current.filter(function(n){return n>=e}).length/(i/1e3)},[y,i]),P=e.useMemo(function(){return{runningTasks:c,completedCount:l,failedCount:p,averageDurationMs:C,throughputPerSecond:M,currentQueueSize:b,totalProcessed:l+p,recentEventCount:y.length}},[c,l,p,C,M,b,y.length]);return{runningTasks:c,completedCount:l,failedCount:p,eventHistory:y,averageDurationMs:C,throughputPerSecond:M,currentQueueSize:b,progress:P}},exports.useWrappedChildren=function(r,t,o){return void 0===o&&(o="preserve"),e.useMemo(function(){if(!r)return r;var e=function(e){if(!n.isValidElement(e))return e;var r,i=e.props||{};r="override"===o?u({},i,t):u({},t,i);var a=null==i?void 0:i.style,c=null==t?void 0:t.style;return a&&c&&"object"==typeof a&&"object"==typeof c&&(r.style="override"===o?u({},a,c):u({},c,a)),n.cloneElement(e,r)};return Array.isArray(r)?r.map(e):e(r)},[r,t,o])};
2
+ //# sourceMappingURL=entry.js.map