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/docs/index.html CHANGED
@@ -1,4 +1,4 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
@@ -8,7 +8,8 @@
8
8
  {
9
9
  "imports": {
10
10
  "preact": "https://esm.sh/preact@10",
11
- "preact/hooks": "https://esm.sh/preact@10/hooks"
11
+ "preact/hooks": "https://esm.sh/preact@10/hooks",
12
+ "preact/compat": "https://esm.sh/preact@10/compat"
12
13
  }
13
14
  }
14
15
  </script>
@@ -25,6 +26,7 @@
25
26
  --surface2: #222228;
26
27
  --border: #2e2e36;
27
28
  --text: #e4e4e7;
29
+ --text2: #d4d4d8;
28
30
  --textMuted: #a1a1aa;
29
31
  --accent: #7c3aed;
30
32
  --accentDim: rgba(124, 58, 237, 0.15);
@@ -38,24 +40,39 @@
38
40
  * {
39
41
  box-sizing: border-box;
40
42
  }
43
+ html {
44
+ overflow-x: hidden;
45
+ }
41
46
  body {
42
47
  margin: 0;
43
- font-family: 'DM Sans', system-ui, sans-serif;
48
+ font-family: "DM Sans", system-ui, sans-serif;
44
49
  background: var(--bg);
45
50
  color: var(--text);
46
51
  line-height: 1.5;
47
52
  min-height: 100vh;
48
53
  }
49
54
  .page-header {
50
- padding: 2rem 1.5rem;
55
+ padding: 2.5rem 1.5rem;
51
56
  text-align: center;
52
57
  border-bottom: 1px solid var(--border);
58
+ background: linear-gradient(
59
+ 180deg,
60
+ rgba(124, 58, 237, 0.04) 0%,
61
+ transparent 100%
62
+ );
53
63
  }
54
64
  .page-header h1 {
55
65
  margin: 0;
56
- font-size: 1.75rem;
66
+ font-size: 1.85rem;
57
67
  font-weight: 700;
58
- letter-spacing: -0.02em;
68
+ letter-spacing: -0.03em;
69
+ color: var(--text2);
70
+ }
71
+ .page-header .subtitle {
72
+ margin: 0.5rem 0 0;
73
+ color: var(--textMuted);
74
+ font-size: 0.95rem;
75
+ font-weight: 400;
59
76
  }
60
77
  .page-header p {
61
78
  margin: 0.5rem 0 0;
@@ -63,39 +80,50 @@
63
80
  font-size: 0.95rem;
64
81
  }
65
82
  .container {
66
- max-width: 1200px;
83
+ max-width: min(1320px, 96vw);
67
84
  margin: 0 auto;
68
- padding: 1.5rem;
85
+ padding: 1.75rem 1.5rem;
69
86
  }
70
87
  .hook-section {
71
- margin-bottom: 3rem;
88
+ margin-bottom: 3.5rem;
89
+ padding-bottom: 2.5rem;
90
+ border-bottom: 1px solid var(--border);
91
+ }
92
+ .hook-section:last-child {
93
+ border-bottom: none;
72
94
  }
73
95
  .hook-section h2 {
74
96
  margin: 0 0 0.5rem;
75
97
  font-size: 1.35rem;
76
98
  font-weight: 600;
77
99
  color: var(--text);
100
+ letter-spacing: -0.02em;
78
101
  }
79
102
  .flow {
80
103
  display: inline-block;
104
+ max-width: 100%;
81
105
  margin-bottom: 0.5rem;
82
- padding: 0.25rem 0.6rem;
106
+ padding: 0.35rem 0.7rem;
83
107
  font-size: 0.75rem;
84
- font-family: 'JetBrains Mono', monospace;
108
+ font-family: "JetBrains Mono", monospace;
85
109
  color: var(--accent);
86
110
  background: var(--accentDim);
87
111
  border-radius: var(--radiusSm);
112
+ white-space: pre-wrap;
113
+ word-break: break-word;
114
+ overflow-wrap: break-word;
88
115
  }
89
116
  .summary {
90
117
  margin: 0 0 1rem;
91
118
  font-size: 0.9rem;
92
119
  color: var(--textMuted);
93
- max-width: 60ch;
120
+ max-width: 72ch;
121
+ line-height: 1.55;
94
122
  }
95
123
  .cards {
96
124
  display: grid;
97
125
  grid-template-columns: 1fr 1fr;
98
- gap: 1rem;
126
+ gap: 1.25rem;
99
127
  }
100
128
  @media (max-width: 800px) {
101
129
  .cards {
@@ -107,6 +135,7 @@
107
135
  border: 1px solid var(--border);
108
136
  border-radius: var(--radius);
109
137
  overflow: hidden;
138
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
110
139
  }
111
140
  .card-title {
112
141
  padding: 0.5rem 0.75rem;
@@ -118,18 +147,24 @@
118
147
  border-bottom: 1px solid var(--border);
119
148
  }
120
149
  .card-code {
121
- padding: 0.75rem;
122
- font-family: 'JetBrains Mono', monospace;
150
+ padding: 0.75rem 1rem;
151
+ font-family: "JetBrains Mono", monospace;
123
152
  font-size: 0.8rem;
124
- line-height: 1.45;
153
+ line-height: 1.5;
125
154
  color: var(--textMuted);
126
155
  background: #0d0d0f;
127
- overflow-x: auto;
128
- white-space: pre;
156
+ white-space: pre-wrap;
157
+ word-break: break-word;
158
+ overflow-wrap: break-word;
159
+ overflow-x: hidden;
160
+ min-width: 0;
129
161
  }
130
162
  .card-live {
131
163
  padding: 1rem;
132
164
  min-height: 80px;
165
+ min-width: 0;
166
+ overflow-wrap: break-word;
167
+ word-break: break-word;
133
168
  }
134
169
  .card-live .status {
135
170
  font-size: 0.85rem;
@@ -215,11 +250,13 @@
215
250
  .wasm-flow-node {
216
251
  padding: 0.3rem 0.5rem;
217
252
  font-size: 0.7rem;
218
- font-family: 'JetBrains Mono', monospace;
253
+ font-family: "JetBrains Mono", monospace;
219
254
  border-radius: 4px;
220
255
  border: 1px solid var(--border);
221
256
  color: var(--textMuted);
222
- transition: background 0.2s, border-color 0.2s;
257
+ transition:
258
+ background 0.2s,
259
+ border-color 0.2s;
223
260
  }
224
261
  .wasm-flow-node.active {
225
262
  border-color: var(--accent);
@@ -291,6 +328,7 @@
291
328
  <body>
292
329
  <header class="page-header">
293
330
  <h1>Preact Missing Hooks</h1>
331
+ <p class="subtitle">React-like hooks for Preact — demo &amp; usage</p>
294
332
  </header>
295
333
  <main class="container" id="root"></main>
296
334
  <script type="module" src="./main.js"></script>
package/docs/main.js CHANGED
@@ -18,6 +18,7 @@ const {
18
18
  useWebRTCIP,
19
19
  useWasmCompute,
20
20
  useWorkerNotifications,
21
+ useLLMMetadata,
21
22
  } = await import(
22
23
  isLocal ? '../dist/index.module.js' : 'https://unpkg.com/preact-missing-hooks/dist/index.module.js'
23
24
  );
@@ -316,6 +317,47 @@ function DemoWorkerNotifications() {
316
317
  );
317
318
  }
318
319
 
320
+ function DemoLLMMetadata() {
321
+ const [route, setRoute] = useState('/');
322
+ useLLMMetadata({
323
+ route,
324
+ mode: 'manual',
325
+ title: 'Preact Missing Hooks — Demo',
326
+ description: 'Live demo of useLLMMetadata and other hooks.',
327
+ tags: ['preact', 'hooks', 'demo'],
328
+ });
329
+ const scriptEl = typeof document !== 'undefined' ? document.querySelector('script[data-llm="true"]') : null;
330
+ const rawText = scriptEl?.textContent ?? '';
331
+ const payload = rawText ? (() => { try { return JSON.parse(rawText); } catch (_) { return null; } })() : null;
332
+ const llmFormatted = payload ? JSON.stringify(payload, null, 2) : rawText || '(no script yet)';
333
+ return h('div', {},
334
+ h('div', { style: { marginBottom: '0.5rem' } }, [
335
+ h('button', { onClick: () => setRoute('/') }, 'Route: /'),
336
+ h('button', { onClick: () => setRoute('/blog') }, 'Route: /blog'),
337
+ h('button', { onClick: () => setRoute('/docs') }, 'Route: /docs'),
338
+ ]),
339
+ h('div', { class: 'status', style: { fontSize: '0.8rem', wordBreak: 'break-all', marginBottom: '0.5rem' } },
340
+ payload
341
+ ? 'Injected: ' + (payload.title || payload.route) + (payload.generatedAt ? ' @ ' + payload.generatedAt : '')
342
+ : 'Check <head> for <script type="application/llm+json" data-llm="true">'
343
+ ),
344
+ h('div', { style: { marginTop: '0.5rem' } }, [
345
+ h('div', { style: { fontSize: '0.75rem', fontWeight: '600', marginBottom: '0.25rem', color: 'var(--text2)' } }, 'llm.txt (full payload below):'),
346
+ h('pre', {
347
+ class: 'card-code',
348
+ style: {
349
+ maxHeight: '12rem',
350
+ overflow: 'auto',
351
+ margin: 0,
352
+ fontSize: '0.7rem',
353
+ whiteSpace: 'pre-wrap',
354
+ wordBreak: 'break-all',
355
+ },
356
+ }, llmFormatted),
357
+ ])
358
+ );
359
+ }
360
+
319
361
  // ——— Page data: heading, flow, summary, code, LiveComponent ———
320
362
 
321
363
  const HOOKS = [
@@ -410,6 +452,13 @@ const HOOKS = [
410
452
  code: `const stats = useWorkerNotifications(worker, { maxHistory: 100 });\n// stats.progress, stats.runningTasks, stats.throughputPerSecond, ...`,
411
453
  Live: DemoWorkerNotifications,
412
454
  },
455
+ {
456
+ name: 'useLLMMetadata',
457
+ flow: 'Component → useLLMMetadata({ route, mode, title?, ... } | null) → <script type="application/llm+json" data-llm="true"> in <head>',
458
+ summary: 'Injects AI-readable metadata into the document head on route change. Manual mode uses config; auto-extract uses document.title, visible h1/h2, first 3 <p>. Accepts null/undefined (minimal payload with route "/"). SSR-safe, cacheable.',
459
+ code: `useLLMMetadata({ route: pathname, mode: 'manual', title: 'My Page', tags: ['app'] });\n// useLLMMetadata(null) is safe → minimal payload with route "/"`,
460
+ Live: DemoLLMMetadata,
461
+ },
413
462
  ];
414
463
 
415
464
  function App() {
@@ -0,0 +1,10 @@
1
+ import eslint from "@eslint/js";
2
+ import { defineConfig } from "eslint/config";
3
+ import tseslint from "typescript-eslint";
4
+ import prettier from "eslint-config-prettier";
5
+
6
+ export default defineConfig(
7
+ eslint.configs.recommended,
8
+ tseslint.configs.recommended,
9
+ prettier
10
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "preact-missing-hooks",
3
- "version": "3.1.0",
3
+ "version": "4.1.0",
4
4
  "description": "A lightweight, extendable collection of missing React-like hooks for Preact — plus fresh, powerful new ones designed specifically for modern Preact apps.",
5
5
  "author": "Prakhar Dubey",
6
6
  "license": "MIT",
@@ -11,8 +11,11 @@
11
11
  "exports": {
12
12
  ".": {
13
13
  "types": "./dist/index.d.ts",
14
- "import": "./dist/index.module.js",
15
- "require": "./dist/index.js"
14
+ "import": {
15
+ "react": "./dist/react.module.js",
16
+ "default": "./dist/index.module.js"
17
+ },
18
+ "require": "./dist/entry.cjs"
16
19
  },
17
20
  "./useTransition": {
18
21
  "types": "./dist/useTransition.d.ts",
@@ -78,16 +81,47 @@
78
81
  "types": "./dist/useWorkerNotifications.d.ts",
79
82
  "import": "./dist/useWorkerNotifications.module.js",
80
83
  "require": "./dist/useWorkerNotifications.js"
84
+ },
85
+ "./react": {
86
+ "types": "./dist/index.d.ts",
87
+ "import": "./dist/react.module.js",
88
+ "require": "./dist/react.js"
81
89
  }
82
90
  },
83
91
  "scripts": {
84
- "build": "microbundle",
92
+ "build": "microbundle --alias react=preact/compat && npm run build:react && node scripts/generate-entry.cjs",
93
+ "build:react": "microbundle -i src/index.ts -o dist/react.js --alias preact=react,preact/hooks=react --no-sourcemap",
85
94
  "dev": "microbundle watch",
86
95
  "prepublishOnly": "npm run build",
87
96
  "test": "vitest run",
97
+ "test:preact": "vitest run --project preact",
98
+ "test:react": "vitest run --project react",
88
99
  "type-check": "tsc --noEmit",
89
- "demo": "npx serve -l 5000"
100
+ "demo": "npx serve -l 5000",
101
+ "lint": "eslint src",
102
+ "format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
103
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,json,md}\"",
104
+ "prepare": "husky",
105
+ "size": "npm run build && size-limit"
90
106
  },
107
+ "size-limit": [
108
+ {
109
+ "path": "dist/index.module.js",
110
+ "limit": "20 KB"
111
+ },
112
+ {
113
+ "path": "dist/index.js",
114
+ "limit": "20 KB"
115
+ },
116
+ {
117
+ "path": "dist/react.module.js",
118
+ "limit": "20 KB"
119
+ },
120
+ {
121
+ "path": "dist/react.js",
122
+ "limit": "20 KB"
123
+ }
124
+ ],
91
125
  "keywords": [
92
126
  "preact",
93
127
  "hooks",
@@ -114,16 +148,36 @@
114
148
  "preact": ">=10.0.0"
115
149
  },
116
150
  "devDependencies": {
151
+ "@eslint/js": "^9.15.0",
152
+ "@size-limit/file": "^11.2.0",
117
153
  "@testing-library/jest-dom": "^6.6.3",
118
154
  "@testing-library/preact": "^3.2.4",
155
+ "@testing-library/react": "^16.0.1",
119
156
  "@types/jest": "^29.5.14",
157
+ "eslint": "^9.15.0",
158
+ "eslint-config-prettier": "^9.1.0",
120
159
  "fake-indexeddb": "^6.0.2",
160
+ "husky": "^9.1.7",
121
161
  "jsdom": "^26.1.0",
122
162
  "microbundle": "^0.15.1",
163
+ "prettier": "^3.3.3",
164
+ "react": "^18.3.1",
165
+ "react-dom": "^18.3.1",
166
+ "size-limit": "^11.2.0",
123
167
  "typescript": "^5.8.3",
168
+ "typescript-eslint": "^8.15.0",
124
169
  "vitest": "^3.1.4"
125
170
  },
126
171
  "peerDependencies": {
127
- "preact": ">=10.0.0"
172
+ "preact": ">=10.0.0",
173
+ "react": ">=17.0.0"
174
+ },
175
+ "peerDependenciesMeta": {
176
+ "preact": {
177
+ "optional": true
178
+ },
179
+ "react": {
180
+ "optional": true
181
+ }
128
182
  }
129
183
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Generates dist/entry.cjs which auto-detects Preact vs React at runtime (CJS only).
3
+ * Run after build so dist/index.js and dist/react.js exist.
4
+ */
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+
8
+ const content = `"use strict";
9
+ /**
10
+ * Auto-detect Preact vs React and re-export the matching build.
11
+ * Used for require('preact-missing-hooks') in Node / CJS bundlers.
12
+ */
13
+ function detect() {
14
+ try {
15
+ require.resolve("preact");
16
+ return require("./index.js");
17
+ } catch (_) {
18
+ try {
19
+ require.resolve("react");
20
+ return require("./react.js");
21
+ } catch (_) {
22
+ throw new Error(
23
+ "preact-missing-hooks: Install either \\"preact\\" or \\"react\\" in your project."
24
+ );
25
+ }
26
+ }
27
+ }
28
+ module.exports = detect();
29
+ `;
30
+
31
+ const outPath = path.join(__dirname, "..", "dist", "entry.cjs");
32
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
33
+ fs.writeFileSync(outPath, content, "utf8");
34
+ console.log("Generated dist/entry.cjs");
package/src/index.ts CHANGED
@@ -1,13 +1,14 @@
1
- export * from './useTransition'
2
- export * from './useMutationObserver'
3
- export * from './useEventBus'
4
- export * from './useWrappedChildren'
5
- export * from './usePreferredTheme'
6
- export * from './useNetworkState'
7
- export * from './useClipboard'
8
- export * from './useRageClick'
9
- export * from './useThreadedWorker'
10
- export * from './useIndexedDB'
11
- export * from './useWebRTCIP'
12
- export * from './useWasmCompute'
13
- export * from './useWorkerNotifications'
1
+ export * from "./useTransition";
2
+ export * from "./useMutationObserver";
3
+ export * from "./useEventBus";
4
+ export * from "./useWrappedChildren";
5
+ export * from "./usePreferredTheme";
6
+ export * from "./useNetworkState";
7
+ export * from "./useClipboard";
8
+ export * from "./useRageClick";
9
+ export * from "./useThreadedWorker";
10
+ export * from "./useIndexedDB";
11
+ export * from "./useWebRTCIP";
12
+ export * from "./useWasmCompute";
13
+ export * from "./useWorkerNotifications";
14
+ export * from "./useLLMMetadata";
@@ -1,92 +1,101 @@
1
- /**
2
- * Database controller: table(name), transaction(storeNames, mode, callback, options).
3
- * @module indexedDB/dbController
4
- */
5
-
6
- import type { IndexedDBConfig, TransactionOptions } from './types';
7
- import type { ITableController } from './tableController';
8
- import { createTableController, createTransactionTableController } from './tableController';
9
-
10
- /** Transaction context passed to the callback: provides table(name) bound to this transaction. */
11
- export interface TransactionContext {
12
- /** Returns a table controller bound to this transaction. Use for all ops inside the callback. */
13
- table: (name: string) => ITableController;
14
- }
15
-
16
- /**
17
- * Database controller built from an open IDBDatabase.
18
- * Exposes table(name) and transaction(...).
19
- */
20
- export interface IDBController {
21
- /** Underlying IDBDatabase (read-only). */
22
- readonly db: IDBDatabase;
23
- /** Returns true if an object store with the given name exists. */
24
- hasTable: (name: string) => boolean;
25
- /** Returns a table controller for the given store (each op opens its own transaction). */
26
- table: (name: string) => ITableController;
27
- /**
28
- * Runs a callback inside a single transaction. All operations in the callback use the same transaction.
29
- * @param storeNames - Object store names to include in the transaction.
30
- * @param mode - 'readonly' | 'readwrite'.
31
- * @param callback - Async or sync function receiving { table(name) }. Return value is ignored; await all ops inside.
32
- * @param options - Optional onSuccess/onError callbacks.
33
- * @returns Promise that resolves when the transaction completes (after all requests and the callback).
34
- */
35
- transaction: <T = void>(
36
- storeNames: string[],
37
- mode: IDBTransactionMode,
38
- callback: (tx: TransactionContext) => T | Promise<T>,
39
- options?: TransactionOptions
40
- ) => Promise<void>;
41
- }
42
-
43
- function withTransactionCallbacks(
44
- promise: Promise<void>,
45
- options?: TransactionOptions
46
- ): Promise<void> {
47
- if (!options) return promise;
48
- return promise
49
- .then(() => options.onSuccess?.())
50
- .catch((err: DOMException) => {
51
- options.onError?.(err);
52
- throw err;
53
- });
54
- }
55
-
56
- /**
57
- * Creates a database controller from an open IDBDatabase instance.
58
- */
59
- export function createDBController(db: IDBDatabase, _config: IndexedDBConfig): IDBController {
60
- return {
61
- get db(): IDBDatabase {
62
- return db;
63
- },
64
-
65
- hasTable(name: string): boolean {
66
- return db.objectStoreNames.contains(name);
67
- },
68
-
69
- table(name: string): ITableController {
70
- return createTableController(db, name);
71
- },
72
-
73
- transaction<T = void>(
74
- storeNames: string[],
75
- mode: IDBTransactionMode,
76
- callback: (tx: TransactionContext) => T | Promise<T>,
77
- options?: TransactionOptions
78
- ): Promise<void> {
79
- const tx = db.transaction(storeNames, mode);
80
- const txContext: TransactionContext = {
81
- table: (tableName: string) => createTransactionTableController(tx, tableName),
82
- };
83
- const txPromise = new Promise<void>((resolve, reject) => {
84
- tx.oncomplete = () => resolve();
85
- tx.onerror = () => reject(tx.error ?? new DOMException('Transaction failed'));
86
- });
87
- const callbackResult = callback(txContext);
88
- const promise = Promise.resolve(callbackResult).then(() => txPromise);
89
- return withTransactionCallbacks(promise, options);
90
- },
91
- };
92
- }
1
+ /**
2
+ * Database controller: table(name), transaction(storeNames, mode, callback, options).
3
+ * @module indexedDB/dbController
4
+ */
5
+
6
+ import type { IndexedDBConfig, TransactionOptions } from "./types";
7
+ import type { ITableController } from "./tableController";
8
+ import {
9
+ createTableController,
10
+ createTransactionTableController,
11
+ } from "./tableController";
12
+
13
+ /** Transaction context passed to the callback: provides table(name) bound to this transaction. */
14
+ export interface TransactionContext {
15
+ /** Returns a table controller bound to this transaction. Use for all ops inside the callback. */
16
+ table: (name: string) => ITableController;
17
+ }
18
+
19
+ /**
20
+ * Database controller built from an open IDBDatabase.
21
+ * Exposes table(name) and transaction(...).
22
+ */
23
+ export interface IDBController {
24
+ /** Underlying IDBDatabase (read-only). */
25
+ readonly db: IDBDatabase;
26
+ /** Returns true if an object store with the given name exists. */
27
+ hasTable: (name: string) => boolean;
28
+ /** Returns a table controller for the given store (each op opens its own transaction). */
29
+ table: (name: string) => ITableController;
30
+ /**
31
+ * Runs a callback inside a single transaction. All operations in the callback use the same transaction.
32
+ * @param storeNames - Object store names to include in the transaction.
33
+ * @param mode - 'readonly' | 'readwrite'.
34
+ * @param callback - Async or sync function receiving { table(name) }. Return value is ignored; await all ops inside.
35
+ * @param options - Optional onSuccess/onError callbacks.
36
+ * @returns Promise that resolves when the transaction completes (after all requests and the callback).
37
+ */
38
+ transaction: <T = void>(
39
+ storeNames: string[],
40
+ mode: IDBTransactionMode,
41
+ callback: (tx: TransactionContext) => T | Promise<T>,
42
+ options?: TransactionOptions
43
+ ) => Promise<void>;
44
+ }
45
+
46
+ function withTransactionCallbacks(
47
+ promise: Promise<void>,
48
+ options?: TransactionOptions
49
+ ): Promise<void> {
50
+ if (!options) return promise;
51
+ return promise
52
+ .then(() => options.onSuccess?.())
53
+ .catch((err: DOMException) => {
54
+ options.onError?.(err);
55
+ throw err;
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Creates a database controller from an open IDBDatabase instance.
61
+ */
62
+ export function createDBController(
63
+ db: IDBDatabase,
64
+ _config: IndexedDBConfig
65
+ ): IDBController {
66
+ void _config; // Reserved for future config options
67
+ return {
68
+ get db(): IDBDatabase {
69
+ return db;
70
+ },
71
+
72
+ hasTable(name: string): boolean {
73
+ return db.objectStoreNames.contains(name);
74
+ },
75
+
76
+ table(name: string): ITableController {
77
+ return createTableController(db, name);
78
+ },
79
+
80
+ transaction<T = void>(
81
+ storeNames: string[],
82
+ mode: IDBTransactionMode,
83
+ callback: (tx: TransactionContext) => T | Promise<T>,
84
+ options?: TransactionOptions
85
+ ): Promise<void> {
86
+ const tx = db.transaction(storeNames, mode);
87
+ const txContext: TransactionContext = {
88
+ table: (tableName: string) =>
89
+ createTransactionTableController(tx, tableName),
90
+ };
91
+ const txPromise = new Promise<void>((resolve, reject) => {
92
+ tx.oncomplete = () => resolve();
93
+ tx.onerror = () =>
94
+ reject(tx.error ?? new DOMException("Transaction failed"));
95
+ });
96
+ const callbackResult = callback(txContext);
97
+ const promise = Promise.resolve(callbackResult).then(() => txPromise);
98
+ return withTransactionCallbacks(promise, options);
99
+ },
100
+ };
101
+ }
@@ -1,11 +1,16 @@
1
- /**
2
- * IndexedDB hook system – public API.
3
- * @module indexedDB
4
- */
5
-
6
- export type { IndexedDBConfig, TableSchema, OperationCallbacks, TransactionOptions } from './types';
7
- export { requestToPromise } from './requestToPromise';
8
- export type { ITableController } from './tableController';
9
- export type { IDBController, TransactionContext } from './dbController';
10
- export { createDBController } from './dbController';
11
- export { openDB } from './openDB';
1
+ /**
2
+ * IndexedDB hook system – public API.
3
+ * @module indexedDB
4
+ */
5
+
6
+ export type {
7
+ IndexedDBConfig,
8
+ TableSchema,
9
+ OperationCallbacks,
10
+ TransactionOptions,
11
+ } from "./types";
12
+ export { requestToPromise } from "./requestToPromise";
13
+ export type { ITableController } from "./tableController";
14
+ export type { IDBController, TransactionContext } from "./dbController";
15
+ export { createDBController } from "./dbController";
16
+ export { openDB } from "./openDB";