preact-missing-hooks 3.1.0 → 4.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.
Files changed (82) 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 +333 -137
  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 +14 -13
  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/useLLMMetadata.d.ts +71 -0
  36. package/dist/useMutationObserver.d.ts +1 -1
  37. package/dist/useNetworkState.d.ts +3 -3
  38. package/dist/usePreferredTheme.d.ts +1 -1
  39. package/dist/useRageClick.d.ts +1 -1
  40. package/dist/useThreadedWorker.d.ts +1 -1
  41. package/dist/useTransition.d.ts +4 -1
  42. package/dist/useWorkerNotifications.d.ts +1 -1
  43. package/dist/useWrappedChildren.d.ts +3 -3
  44. package/docs/README.md +111 -0
  45. package/docs/index.html +58 -20
  46. package/docs/main.js +49 -0
  47. package/eslint.config.mjs +10 -0
  48. package/package.json +60 -6
  49. package/scripts/generate-entry.cjs +34 -0
  50. package/src/index.ts +14 -13
  51. package/src/indexedDB/dbController.ts +101 -92
  52. package/src/indexedDB/index.ts +16 -11
  53. package/src/indexedDB/openDB.ts +49 -49
  54. package/src/indexedDB/requestToPromise.ts +17 -16
  55. package/src/indexedDB/tableController.ts +331 -257
  56. package/src/indexedDB/types.ts +35 -35
  57. package/src/useClipboard.ts +99 -97
  58. package/src/useEventBus.ts +39 -36
  59. package/src/useIndexedDB.ts +111 -111
  60. package/src/useLLMMetadata.ts +418 -0
  61. package/src/useMutationObserver.ts +26 -26
  62. package/src/useNetworkState.ts +124 -122
  63. package/src/usePreferredTheme.ts +68 -68
  64. package/src/useRageClick.ts +103 -103
  65. package/src/useThreadedWorker.ts +165 -165
  66. package/src/useTransition.ts +22 -19
  67. package/src/useWasmCompute.ts +209 -204
  68. package/src/useWebRTCIP.ts +181 -176
  69. package/src/useWorkerNotifications.ts +28 -20
  70. package/src/useWrappedChildren.ts +72 -58
  71. package/tests/preact-as-react.ts +5 -0
  72. package/tests/react-adapter.tsx +12 -0
  73. package/tests/setup-react.ts +4 -0
  74. package/tests/useClipboard.test.tsx +4 -2
  75. package/tests/useLLMMetadata.test.tsx +149 -0
  76. package/tests/useThreadedWorker.test.tsx +3 -1
  77. package/tests/useWasmCompute.test.tsx +1 -1
  78. package/tests/useWebRTCIP.test.tsx +3 -1
  79. package/vite.config.ts +11 -4
  80. package/vitest.config.preact.ts +21 -0
  81. package/vitest.config.react.ts +36 -0
  82. package/vitest.workspace.ts +6 -0
package/Readme.md CHANGED
@@ -34,9 +34,10 @@ A lightweight, extendable collection of React-like hooks for Preact, including u
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
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
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.
37
+ - **`useLLMMetadata`** — Injects an AI-readable metadata block into the document head on route change. Works in React 18+ and Preact 10+. Supports **manual** (title, description, tags) and **auto-extract** (from `document.title`, visible `h1`/`h2`, first 3 `p`). Cacheable, SSR-safe, no router dependency.
37
38
  - Fully TypeScript compatible
38
39
  - Bundled with Microbundle
39
- - Zero dependencies (except `preact`)
40
+ - Zero dependencies (peer: `preact` or `react` — use `/react` for React)
40
41
 
41
42
  ---
42
43
 
@@ -46,23 +47,103 @@ A lightweight, extendable collection of React-like hooks for Preact, including u
46
47
  npm install preact-missing-hooks
47
48
  ```
48
49
 
50
+ Ensure your app has either **preact** or **react** installed (the package uses whichever is present).
51
+
49
52
  ---
50
53
 
51
54
  ## Import options
52
55
 
53
- - **Main package** Import from the package root:
54
- ```ts
55
- import { useThreadedWorker, useClipboard } from 'preact-missing-hooks'
56
- ```
56
+ Use the same import in Preact and React projects:
57
+
58
+ ```ts
59
+ import { useThreadedWorker, useClipboard } from "preact-missing-hooks";
60
+ ```
61
+
62
+ - **How it picks Preact vs React**
63
+ - **CommonJS / Node:** The package detects which of `preact` or `react` is installed and uses that build automatically.
64
+ - **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:
65
+ - **Vite:** `vite.config.ts` → `resolve: { conditions: ['react'] }`
66
+ - **Webpack:** `resolve.conditionNames` (or similar) to include `'react'`
67
+ - **Or** in React projects you can always import from the explicit entry: `preact-missing-hooks/react`.
68
+
57
69
  - **Subpath exports (tree-shakeable)** — Import a single hook:
70
+
58
71
  ```ts
59
- import { useThreadedWorker } from 'preact-missing-hooks/useThreadedWorker'
60
- import { useClipboard } from 'preact-missing-hooks/useClipboard'
61
- import { useWebRTCIP } from 'preact-missing-hooks/useWebRTCIP'
62
- import { useWasmCompute } from 'preact-missing-hooks/useWasmCompute'
63
- import { useWorkerNotifications } from 'preact-missing-hooks/useWorkerNotifications'
72
+ import { useThreadedWorker } from "preact-missing-hooks/useThreadedWorker";
73
+ import { useClipboard } from "preact-missing-hooks/useClipboard";
74
+ import { useWebRTCIP } from "preact-missing-hooks/useWebRTCIP";
75
+ import { useWasmCompute } from "preact-missing-hooks/useWasmCompute";
76
+ import { useWorkerNotifications } from "preact-missing-hooks/useWorkerNotifications";
64
77
  ```
65
- All hooks are available: `useTransition`, `useMutationObserver`, `useEventBus`, `useWrappedChildren`, `usePreferredTheme`, `useNetworkState`, `useClipboard`, `useRageClick`, `useThreadedWorker`, `useIndexedDB`, `useWebRTCIP`, `useWasmCompute`, `useWorkerNotifications`.
78
+
79
+ All hooks are available: `useTransition`, `useMutationObserver`, `useEventBus`, `useWrappedChildren`, `usePreferredTheme`, `useNetworkState`, `useClipboard`, `useRageClick`, `useThreadedWorker`, `useIndexedDB`, `useWebRTCIP`, `useWasmCompute`, `useWorkerNotifications`, `useLLMMetadata`.
80
+
81
+ ---
82
+
83
+ ## Quick start
84
+
85
+ Minimal example (Preact or React):
86
+
87
+ ```tsx
88
+ import {
89
+ useTransition,
90
+ useClipboard,
91
+ usePreferredTheme,
92
+ } from "preact-missing-hooks";
93
+
94
+ function App() {
95
+ const [startTransition, isPending] = useTransition();
96
+ const { copy, copied } = useClipboard();
97
+ const theme = usePreferredTheme();
98
+
99
+ return (
100
+ <div>
101
+ <button
102
+ onClick={() =>
103
+ startTransition(() => {
104
+ /* heavy update */
105
+ })
106
+ }
107
+ disabled={isPending}
108
+ >
109
+ {isPending ? "Loading…" : "Update"}
110
+ </button>
111
+ <button onClick={() => copy("Hello!")}>
112
+ {copied ? "Copied!" : "Copy"}
113
+ </button>
114
+ <span>Theme: {theme}</span>
115
+ </div>
116
+ );
117
+ }
118
+ ```
119
+
120
+ **Live demo:** Run the docs demo locally to try every hook with live examples:
121
+
122
+ ```bash
123
+ npm run build && npx serve -l 5000
124
+ # Open http://localhost:5000/docs/
125
+ ```
126
+
127
+ Or open `docs/index.html` after building (see [docs/README.md](docs/README.md) for details).
128
+
129
+ **Usage at a glance:**
130
+
131
+ | Hook | One-liner |
132
+ | ------------------------------------------------- | --------------------------------------------------------------------------------- |
133
+ | [useTransition](#usetransition) | `const [startTransition, isPending] = useTransition();` |
134
+ | [useMutationObserver](#usemutationobserver) | `useMutationObserver(ref, callback, { childList: true });` |
135
+ | [useEventBus](#useeventbus) | `const { emit, on } = useEventBus();` |
136
+ | [useWrappedChildren](#usewrappedchildren) | `const wrapped = useWrappedChildren(children, { className: 'x' });` |
137
+ | [usePreferredTheme](#usepreferredtheme) | `const theme = usePreferredTheme(); // 'light' \| 'dark' \| 'no-preference'` |
138
+ | [useNetworkState](#usenetworkstate) | `const { online, effectiveType } = useNetworkState();` |
139
+ | [useClipboard](#useclipboard) | `const { copy, paste, copied } = useClipboard();` |
140
+ | [useRageClick](#userageclick) | `useRageClick(ref, { onRageClick, threshold: 5 });` |
141
+ | [useThreadedWorker](#usethreadedworker) | `const { run, loading, result } = useThreadedWorker(fn, { mode: 'sequential' });` |
142
+ | [useIndexedDB](#useindexeddb) | `const { db, isReady } = useIndexedDB({ name, version, tables });` |
143
+ | [useWebRTCIP](#usewebrtcip) | `const { ips, loading, error } = useWebRTCIP({ timeout: 3000 });` |
144
+ | [useWasmCompute](#usewasmcompute) | `const { compute, result, ready } = useWasmCompute({ wasmUrl });` |
145
+ | [useWorkerNotifications](#useworkernotifications) | `const { progress, eventHistory } = useWorkerNotifications(worker);` |
146
+ | [useLLMMetadata](#usellmmetadata) | `useLLMMetadata({ route: pathname, mode: 'auto-extract' });` |
66
147
 
67
148
  ---
68
149
 
@@ -71,22 +152,22 @@ npm install preact-missing-hooks
71
152
  ### `useTransition`
72
153
 
73
154
  ```tsx
74
- import { useTransition } from 'preact-missing-hooks'
155
+ import { useTransition } from "preact-missing-hooks";
75
156
 
76
157
  function ExampleTransition() {
77
- const [startTransition, isPending] = useTransition()
158
+ const [startTransition, isPending] = useTransition();
78
159
 
79
160
  const handleClick = () => {
80
161
  startTransition(() => {
81
162
  // perform an expensive update here
82
- })
83
- }
163
+ });
164
+ };
84
165
 
85
166
  return (
86
167
  <button onClick={handleClick} disabled={isPending}>
87
- {isPending ? 'Loading...' : 'Click Me'}
168
+ {isPending ? "Loading..." : "Click Me"}
88
169
  </button>
89
- )
170
+ );
90
171
  }
91
172
  ```
92
173
 
@@ -95,21 +176,21 @@ function ExampleTransition() {
95
176
  ### `useMutationObserver`
96
177
 
97
178
  ```tsx
98
- import { useRef } from 'preact/hooks'
99
- import { useMutationObserver } from 'preact-missing-hooks'
179
+ import { useRef } from "preact/hooks";
180
+ import { useMutationObserver } from "preact-missing-hooks";
100
181
 
101
182
  function ExampleMutation() {
102
- const ref = useRef<HTMLDivElement>(null)
183
+ const ref = useRef<HTMLDivElement>(null);
103
184
 
104
185
  useMutationObserver(
105
186
  ref,
106
187
  (mutations) => {
107
- console.log('Detected mutations:', mutations)
188
+ console.log("Detected mutations:", mutations);
108
189
  },
109
- { childList: true, subtree: true },
110
- )
190
+ { childList: true, subtree: true }
191
+ );
111
192
 
112
- return <div ref={ref}>Observe this content</div>
193
+ return <div ref={ref}>Observe this content</div>;
113
194
  }
114
195
  ```
115
196
 
@@ -120,33 +201,33 @@ function ExampleMutation() {
120
201
  ```tsx
121
202
  // types.ts
122
203
  export type Events = {
123
- notify: (message: string) => void
124
- }
204
+ notify: (message: string) => void;
205
+ };
125
206
 
126
207
  // Sender.tsx
127
- import { useEventBus } from 'preact-missing-hooks'
128
- import type { Events } from './types'
208
+ import { useEventBus } from "preact-missing-hooks";
209
+ import type { Events } from "./types";
129
210
 
130
211
  function Sender() {
131
- const { emit } = useEventBus<Events>()
132
- return <button onClick={() => emit('notify', 'Hello World!')}>Send</button>
212
+ const { emit } = useEventBus<Events>();
213
+ return <button onClick={() => emit("notify", "Hello World!")}>Send</button>;
133
214
  }
134
215
 
135
216
  // Receiver.tsx
136
- import { useEventBus } from 'preact-missing-hooks'
137
- import { useState, useEffect } from 'preact/hooks'
138
- import type { Events } from './types'
217
+ import { useEventBus } from "preact-missing-hooks";
218
+ import { useState, useEffect } from "preact/hooks";
219
+ import type { Events } from "./types";
139
220
 
140
221
  function Receiver() {
141
- const [msg, setMsg] = useState<string>('')
142
- const { on } = useEventBus<Events>()
222
+ const [msg, setMsg] = useState<string>("");
223
+ const { on } = useEventBus<Events>();
143
224
 
144
225
  useEffect(() => {
145
- const unsubscribe = on('notify', setMsg)
146
- return unsubscribe
147
- }, [])
226
+ const unsubscribe = on("notify", setMsg);
227
+ return unsubscribe;
228
+ }, []);
148
229
 
149
- return <div>Message: {msg}</div>
230
+ return <div>Message: {msg}</div>;
150
231
  }
151
232
  ```
152
233
 
@@ -155,19 +236,19 @@ function Receiver() {
155
236
  ### `useWrappedChildren`
156
237
 
157
238
  ```tsx
158
- import { useWrappedChildren } from 'preact-missing-hooks'
239
+ import { useWrappedChildren } from "preact-missing-hooks";
159
240
 
160
241
  function ParentComponent({ children }) {
161
242
  // Inject common props into all children
162
243
  const injectProps = {
163
- className: 'enhanced-child',
164
- onClick: () => console.log('Child clicked!'),
165
- style: { border: '1px solid #ccc' },
166
- }
244
+ className: "enhanced-child",
245
+ onClick: () => console.log("Child clicked!"),
246
+ style: { border: "1px solid #ccc" },
247
+ };
167
248
 
168
- const wrappedChildren = useWrappedChildren(children, injectProps)
249
+ const wrappedChildren = useWrappedChildren(children, injectProps);
169
250
 
170
- return <div className="parent">{wrappedChildren}</div>
251
+ return <div className="parent">{wrappedChildren}</div>;
171
252
  }
172
253
 
173
254
  // Usage with preserve strategy (default - existing props are preserved)
@@ -175,21 +256,21 @@ function PreserveExample() {
175
256
  return (
176
257
  <ParentComponent>
177
258
  <button className="btn">Existing class preserved</button>
178
- <span style={{ color: 'red' }}>Both styles applied</span>
259
+ <span style={{ color: "red" }}>Both styles applied</span>
179
260
  </ParentComponent>
180
- )
261
+ );
181
262
  }
182
263
 
183
264
  // Usage with override strategy (injected props override existing ones)
184
265
  function OverrideExample() {
185
- const injectProps = { className: 'new-class' }
266
+ const injectProps = { className: "new-class" };
186
267
  const children = (
187
268
  <button className="old-class">Class will be overridden</button>
188
- )
269
+ );
189
270
 
190
- const wrappedChildren = useWrappedChildren(children, injectProps, 'override')
271
+ const wrappedChildren = useWrappedChildren(children, injectProps, "override");
191
272
 
192
- return <div>{wrappedChildren}</div>
273
+ return <div>{wrappedChildren}</div>;
193
274
  }
194
275
  ```
195
276
 
@@ -198,12 +279,12 @@ function OverrideExample() {
198
279
  ### `usePreferredTheme`
199
280
 
200
281
  ```tsx
201
- import { usePreferredTheme } from 'preact-missing-hooks'
282
+ import { usePreferredTheme } from "preact-missing-hooks";
202
283
 
203
284
  function ThemeAwareComponent() {
204
- const theme = usePreferredTheme() // 'light' | 'dark' | 'no-preference'
285
+ const theme = usePreferredTheme(); // 'light' | 'dark' | 'no-preference'
205
286
 
206
- return <div data-theme={theme}>Your system prefers: {theme}</div>
287
+ return <div data-theme={theme}>Your system prefers: {theme}</div>;
207
288
  }
208
289
  ```
209
290
 
@@ -212,18 +293,18 @@ function ThemeAwareComponent() {
212
293
  ### `useNetworkState`
213
294
 
214
295
  ```tsx
215
- import { useNetworkState } from 'preact-missing-hooks'
296
+ import { useNetworkState } from "preact-missing-hooks";
216
297
 
217
298
  function NetworkStatus() {
218
- const { online, effectiveType, saveData } = useNetworkState()
299
+ const { online, effectiveType, saveData } = useNetworkState();
219
300
 
220
301
  return (
221
302
  <div>
222
- Status: {online ? 'Online' : 'Offline'}
303
+ Status: {online ? "Online" : "Offline"}
223
304
  {effectiveType && ` (${effectiveType})`}
224
- {saveData && ' — Reduced data mode enabled'}
305
+ {saveData && " — Reduced data mode enabled"}
225
306
  </div>
226
- )
307
+ );
227
308
  }
228
309
  ```
229
310
 
@@ -232,34 +313,34 @@ function NetworkStatus() {
232
313
  ### `useClipboard`
233
314
 
234
315
  ```tsx
235
- import { useState } from 'preact/hooks'
236
- import { useClipboard } from 'preact-missing-hooks'
316
+ import { useState } from "preact/hooks";
317
+ import { useClipboard } from "preact-missing-hooks";
237
318
 
238
319
  function CopyButton() {
239
- const { copy, copied, error } = useClipboard({ resetDelay: 2000 })
320
+ const { copy, copied, error } = useClipboard({ resetDelay: 2000 });
240
321
 
241
322
  return (
242
- <button onClick={() => copy('Hello, World!')}>
243
- {copied ? 'Copied!' : 'Copy'}
323
+ <button onClick={() => copy("Hello, World!")}>
324
+ {copied ? "Copied!" : "Copy"}
244
325
  </button>
245
- )
326
+ );
246
327
  }
247
328
 
248
329
  function PasteInput() {
249
- const [text, setText] = useState('')
250
- const { paste } = useClipboard()
330
+ const [text, setText] = useState("");
331
+ const { paste } = useClipboard();
251
332
 
252
333
  const handlePaste = async () => {
253
- const content = await paste()
254
- setText(content)
255
- }
334
+ const content = await paste();
335
+ setText(content);
336
+ };
256
337
 
257
338
  return (
258
339
  <div>
259
340
  <input value={text} onChange={(e) => setText(e.target.value)} />
260
341
  <button onClick={handlePaste}>Paste</button>
261
342
  </div>
262
- )
343
+ );
263
344
  }
264
345
  ```
265
346
 
@@ -270,26 +351,26 @@ function PasteInput() {
270
351
  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.
271
352
 
272
353
  ```tsx
273
- import { useRef } from 'preact/hooks'
274
- import { useRageClick } from 'preact-missing-hooks'
354
+ import { useRef } from "preact/hooks";
355
+ import { useRageClick } from "preact-missing-hooks";
275
356
 
276
357
  function SubmitButton() {
277
- const ref = useRef<HTMLButtonElement>(null)
358
+ const ref = useRef<HTMLButtonElement>(null);
278
359
 
279
360
  useRageClick(ref, {
280
361
  onRageClick: ({ count, event }) => {
281
362
  // Report to Sentry (or your error tracker) to create rage-click issues
282
- Sentry.captureMessage('Rage click detected', {
283
- level: 'warning',
284
- extra: { count, target: event.target, tag: 'rage_click' },
285
- })
363
+ Sentry.captureMessage("Rage click detected", {
364
+ level: "warning",
365
+ extra: { count, target: event.target, tag: "rage_click" },
366
+ });
286
367
  },
287
368
  threshold: 5, // min clicks (default 5, Sentry-style)
288
369
  timeWindow: 1000, // ms (default 1000)
289
370
  distanceThreshold: 30, // px (default 30)
290
- })
371
+ });
291
372
 
292
- return <button ref={ref}>Submit</button>
373
+ return <button ref={ref}>Submit</button>;
293
374
  }
294
375
  ```
295
376
 
@@ -300,16 +381,16 @@ function SubmitButton() {
300
381
  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.
301
382
 
302
383
  ```tsx
303
- import { useThreadedWorker } from 'preact-missing-hooks'
384
+ import { useThreadedWorker } from "preact-missing-hooks";
304
385
 
305
386
  // Sequential: one task at a time, sorted by priority
306
- const sequential = useThreadedWorker(fetchUser, { mode: 'sequential' })
387
+ const sequential = useThreadedWorker(fetchUser, { mode: "sequential" });
307
388
 
308
389
  // Parallel: up to N tasks at once
309
390
  const parallel = useThreadedWorker(processItem, {
310
- mode: 'parallel',
391
+ mode: "parallel",
311
392
  concurrency: 4,
312
- })
393
+ });
313
394
 
314
395
  // API (same for both modes)
315
396
  const {
@@ -320,11 +401,11 @@ const {
320
401
  queueSize, // tasks queued + running
321
402
  clearQueue, // clear pending tasks (running continue)
322
403
  terminate, // clear queue and reject new run()
323
- } = sequential
404
+ } = sequential;
324
405
 
325
406
  // Run with priority (1 = highest)
326
- await run({ userId: 1 }, { priority: 1 })
327
- await run({ userId: 2 }, { priority: 3 })
407
+ await run({ userId: 1 }, { priority: 1 });
408
+ await run({ userId: 2 }, { priority: 3 });
328
409
  ```
329
410
 
330
411
  ---
@@ -340,40 +421,40 @@ Production-ready IndexedDB hook: database initialization, table creation (with k
340
421
  **Database API:** `db.table(name)`, `db.hasTable(name)`, `db.transaction(storeNames, mode, callback, options?)`.
341
422
 
342
423
  ```tsx
343
- import { useIndexedDB } from 'preact-missing-hooks'
424
+ import { useIndexedDB } from "preact-missing-hooks";
344
425
 
345
426
  function App() {
346
427
  const { db, isReady, error } = useIndexedDB({
347
- name: 'my-app-db',
428
+ name: "my-app-db",
348
429
  version: 1,
349
430
  tables: {
350
- users: { keyPath: 'id', autoIncrement: true, indexes: ['email'] },
351
- settings: { keyPath: 'key' },
431
+ users: { keyPath: "id", autoIncrement: true, indexes: ["email"] },
432
+ settings: { keyPath: "key" },
352
433
  },
353
- })
434
+ });
354
435
 
355
- if (error) return <div>Failed to open database</div>
356
- if (!isReady || !db) return <div>Loading...</div>
436
+ if (error) return <div>Failed to open database</div>;
437
+ if (!isReady || !db) return <div>Loading...</div>;
357
438
 
358
- const users = db.table('users')
439
+ const users = db.table("users");
359
440
 
360
441
  // All operations return Promises and accept optional { onSuccess, onError }
361
- await users.insert({ email: 'a@b.com', name: 'Alice' })
362
- await users.update(1, { name: 'Alice Smith' })
363
- const found = await users.query((u) => u.email.startsWith('a@'))
364
- const n = await users.count()
365
- await users.delete(1)
366
- await users.upsert({ id: 2, email: 'b@b.com' })
367
- await users.bulkInsert([{ email: 'c@b.com' }, { email: 'd@b.com' }])
368
- await users.clear()
442
+ await users.insert({ email: "a@b.com", name: "Alice" });
443
+ await users.update(1, { name: "Alice Smith" });
444
+ const found = await users.query((u) => u.email.startsWith("a@"));
445
+ const n = await users.count();
446
+ await users.delete(1);
447
+ await users.upsert({ id: 2, email: "b@b.com" });
448
+ await users.bulkInsert([{ email: "c@b.com" }, { email: "d@b.com" }]);
449
+ await users.clear();
369
450
 
370
451
  // Full transaction support
371
- await db.transaction(['users', 'settings'], 'readwrite', async (tx) => {
372
- await tx.table('users').insert({ email: 'e@b.com' })
373
- await tx.table('settings').upsert({ key: 'theme', value: 'dark' })
374
- })
452
+ await db.transaction(["users", "settings"], "readwrite", async (tx) => {
453
+ await tx.table("users").insert({ email: "e@b.com" });
454
+ await tx.table("settings").upsert({ key: "theme", value: "dark" });
455
+ });
375
456
 
376
- return <div>DB ready. Tables: {db.hasTable('users') ? 'users' : ''}</div>
457
+ return <div>DB ready. Tables: {db.hasTable("users") ? "users" : ""}</div>;
377
458
  }
378
459
  ```
379
460
 
@@ -386,8 +467,8 @@ Detects client IP addresses using WebRTC ICE candidates and a STUN server (**fro
386
467
  Returns `{ ips: string[], loading: boolean, error: string | null }`. Options: `stunServers`, `timeout` (ms), `onDetect(ip)`.
387
468
 
388
469
  ```tsx
389
- import { useWebRTCIP } from 'preact-missing-hooks'
390
- import { useState, useEffect } from 'preact/hooks'
470
+ import { useWebRTCIP } from "preact-missing-hooks";
471
+ import { useState, useEffect } from "preact/hooks";
391
472
 
392
473
  function ClientIP() {
393
474
  const { ips, loading, error } = useWebRTCIP({
@@ -395,25 +476,25 @@ function ClientIP() {
395
476
  onDetect: (ip) => {
396
477
  /* optional: e.g. analytics */
397
478
  },
398
- })
399
- const [fallbackIP, setFallbackIP] = useState<string | null>(null)
479
+ });
480
+ const [fallbackIP, setFallbackIP] = useState<string | null>(null);
400
481
 
401
482
  // Fallback to public IP API when WebRTC fails or returns empty
402
483
  useEffect(() => {
403
- if (loading || ips.length > 0) return
484
+ if (loading || ips.length > 0) return;
404
485
  if (error) {
405
- fetch('https://api.ipify.org?format=json')
486
+ fetch("https://api.ipify.org?format=json")
406
487
  .then((r) => r.json())
407
488
  .then((d) => setFallbackIP(d.ip))
408
- .catch(() => {})
489
+ .catch(() => {});
409
490
  }
410
- }, [loading, ips.length, error])
491
+ }, [loading, ips.length, error]);
411
492
 
412
- if (loading) return <p>Detecting IP…</p>
413
- if (ips.length > 0) return <p>IPs (WebRTC): {ips.join(', ')}</p>
414
- if (fallbackIP) return <p>IP (fallback API): {fallbackIP}</p>
415
- if (error) return <p>WebRTC failed. Try fallback API.</p>
416
- return null
493
+ if (loading) return <p>Detecting IP…</p>;
494
+ if (ips.length > 0) return <p>IPs (WebRTC): {ips.join(", ")}</p>;
495
+ if (fallbackIP) return <p>IP (fallback API): {fallbackIP}</p>;
496
+ if (error) return <p>WebRTC failed. Try fallback API.</p>;
497
+ return null;
417
498
  }
418
499
  ```
419
500
 
@@ -426,23 +507,23 @@ Runs WebAssembly computation in a Web Worker so the main thread stays responsive
426
507
  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).
427
508
 
428
509
  ```tsx
429
- import { useWasmCompute } from 'preact-missing-hooks'
510
+ import { useWasmCompute } from "preact-missing-hooks";
430
511
 
431
512
  function AddWithWasm() {
432
513
  const { compute, result, loading, error, ready } = useWasmCompute<
433
514
  number,
434
515
  number
435
516
  >({
436
- wasmUrl: '/add.wasm',
437
- exportName: 'add',
438
- })
517
+ wasmUrl: "/add.wasm",
518
+ exportName: "add",
519
+ });
439
520
 
440
521
  const handleClick = () => {
441
- if (ready) compute(2).then(() => {})
442
- }
522
+ if (ready) compute(2).then(() => {});
523
+ };
443
524
 
444
- if (error) return <p>WASM unavailable: {error}</p>
445
- if (!ready) return <p>Loading WASM…</p>
525
+ if (error) return <p>WASM unavailable: {error}</p>;
526
+ if (!ready) return <p>Loading WASM…</p>;
446
527
  return (
447
528
  <div>
448
529
  <button onClick={handleClick} disabled={loading}>
@@ -450,7 +531,7 @@ function AddWithWasm() {
450
531
  </button>
451
532
  {result != null && <p>Result: {result}</p>}
452
533
  </div>
453
- )
534
+ );
454
535
  }
455
536
  ```
456
537
 
@@ -463,27 +544,142 @@ Listens to a Worker's `message` events and maintains state and derived stats. Yo
463
544
  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).
464
545
 
465
546
  ```tsx
466
- import { useWorkerNotifications } from 'preact-missing-hooks'
547
+ import { useWorkerNotifications } from "preact-missing-hooks";
467
548
 
468
549
  function WorkerDashboard({ worker }) {
469
550
  const { progress, eventHistory } = useWorkerNotifications(worker, {
470
551
  maxHistory: 50,
471
- })
552
+ });
472
553
 
473
554
  return (
474
555
  <div>
475
556
  <p>
476
- Running: {progress.runningTasks.length} | Done:{' '}
557
+ Running: {progress.runningTasks.length} | Done:{" "}
477
558
  {progress.completedCount} | Failed: {progress.failedCount}
478
559
  </p>
479
560
  <p>
480
- Avg: {progress.averageDurationMs.toFixed(0)}ms | Throughput:{' '}
481
- {progress.throughputPerSecond.toFixed(2)}/s | Queue:{' '}
561
+ Avg: {progress.averageDurationMs.toFixed(0)}ms | Throughput:{" "}
562
+ {progress.throughputPerSecond.toFixed(2)}/s | Queue:{" "}
482
563
  {progress.currentQueueSize}
483
564
  </p>
484
565
  <small>Events: {eventHistory.length}</small>
485
566
  </div>
486
- )
567
+ );
568
+ }
569
+ ```
570
+
571
+ ---
572
+
573
+ ### `useLLMMetadata`
574
+
575
+ Injects an AI-readable metadata block into the document head when the route changes. Works in **React 18+** and **Preact 10+** (framework-agnostic). No router dependency — you pass the current `route` string and the hook updates the script when it changes.
576
+
577
+ **Safe usage:** The hook **never throws**. It accepts `config` or `null`/`undefined`. When `config` is `null` or `undefined`, it injects a minimal payload with `route: "/"` and `generatedAt`. Invalid or missing values are normalized; all strings are length-limited and URLs validated; DOM access is wrapped in try/catch. Safe for SSR (no-op when `window` is undefined).
578
+
579
+ **API:**
580
+
581
+ ```ts
582
+ type OGType =
583
+ | "website"
584
+ | "article"
585
+ | "profile"
586
+ | "video.other"
587
+ | "product"
588
+ | "music.song"
589
+ | "book";
590
+
591
+ interface LLMConfig {
592
+ route: string;
593
+ mode?: "manual" | "auto-extract";
594
+ title?: string;
595
+ description?: string;
596
+ tags?: string[];
597
+ canonicalUrl?: string; // absolute URL
598
+ language?: string; // e.g. "en", "en-US"
599
+ ogType?: OGType; // Open Graph type
600
+ ogImage?: string; // absolute image URL
601
+ ogImageAlt?: string;
602
+ siteName?: string;
603
+ author?: string;
604
+ publishedTime?: string; // ISO date
605
+ modifiedTime?: string; // ISO date
606
+ robots?: string; // e.g. "index, follow"
607
+ extra?: Record<string, string | number | boolean | string[]>;
608
+ }
609
+
610
+ function useLLMMetadata(config: LLMConfig | null | undefined): void;
611
+ ```
612
+
613
+ **Behavior:**
614
+
615
+ - When `config` is `null` or `undefined`: injects a minimal payload with `route: "/"` and `generatedAt` (no throw).
616
+ - When `config.route` (or other deps) change: removes any existing `<script data-llm="true">`, then injects a new one.
617
+ - Script tag: `<script type="application/llm+json" data-llm="true">` with JSON payload. Only defined, safe fields are included.
618
+ - **Cacheable:** If the generated payload is unchanged, the script is not replaced.
619
+ - **SSR-safe:** No-op when `typeof window === "undefined"`.
620
+ - Cleans up on unmount (removes the script).
621
+
622
+ **Modes:**
623
+
624
+ - **`manual`** (default): Uses `title`, `description`, `tags`, and any other config fields you pass.
625
+ - **`auto-extract`**: Fills `title`, `description`, and `outline` from the DOM (`document.title`, visible `<h1>`/`<h2>`, first 3 visible `<p>`). You can still override with config. Ignores content inside `nav`, `footer`, `script`, `style`.
626
+
627
+ **Example payload (rich):**
628
+
629
+ ```json
630
+ {
631
+ "route": "/blog/ai-hooks",
632
+ "title": "AI Hooks in Preact",
633
+ "description": "A short summary...",
634
+ "tags": ["preact", "react", "hooks"],
635
+ "outline": ["Intro", "Problem", "Solution"],
636
+ "canonicalUrl": "https://example.com/blog/ai-hooks",
637
+ "language": "en",
638
+ "ogType": "article",
639
+ "ogImage": "https://example.com/og.png",
640
+ "siteName": "My Blog",
641
+ "author": "Jane Doe",
642
+ "publishedTime": "2025-02-14T10:00:00.000Z",
643
+ "modifiedTime": "2025-02-14T12:00:00.000Z",
644
+ "robots": "index, follow",
645
+ "generatedAt": "2025-02-14T12:00:00.000Z"
646
+ }
647
+ ```
648
+
649
+ **Example: React Router**
650
+
651
+ ```tsx
652
+ import { useLocation } from "react-router-dom";
653
+ import { useLLMMetadata } from "preact-missing-hooks"; // or "preact-missing-hooks/react"
654
+
655
+ function App() {
656
+ const { pathname } = useLocation();
657
+ useLLMMetadata({
658
+ route: pathname,
659
+ mode: "auto-extract",
660
+ title: document.title,
661
+ tags: ["my-app"],
662
+ });
663
+ return <Outlet />;
664
+ }
665
+ ```
666
+
667
+ **Example: Preact Router**
668
+
669
+ ```tsx
670
+ import { useLocation } from "preact-router";
671
+ import { useLLMMetadata } from "preact-missing-hooks";
672
+
673
+ function App() {
674
+ const [pathname] = useLocation();
675
+ useLLMMetadata({
676
+ route: pathname ?? "/",
677
+ mode: "manual",
678
+ title: "My Page",
679
+ description: "Page description",
680
+ tags: ["preact", "hooks"],
681
+ });
682
+ return <div>{/* your routes / children */}</div>;
487
683
  }
488
684
  ```
489
685