preact-missing-hooks 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Wraps an IDBRequest in a Promise.
3
+ * @module indexedDB/requestToPromise
4
+ */
5
+ /**
6
+ * Converts an IDBRequest to a Promise. Rejects with the request's error on failure.
7
+ * @param request - Native IndexedDB request.
8
+ * @returns Promise that resolves with the request result or rejects with DOMException.
9
+ */
10
+ export declare function requestToPromise<T>(request: IDBRequest<T>): Promise<T>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Table controller: insert, update, delete, exists, query, upsert, bulkInsert, clear, count.
3
+ * Works in standalone mode (opens its own transaction per op) or bound to a transaction.
4
+ * @module indexedDB/tableController
5
+ */
6
+ import type { OperationCallbacks } from './types';
7
+ /** Public interface for a table controller (standalone or transaction-scoped). */
8
+ export interface ITableController {
9
+ insert<T>(data: T, options?: OperationCallbacks<IDBValidKey>): Promise<IDBValidKey>;
10
+ update<T>(key: IDBValidKey, updates: Partial<T>, options?: OperationCallbacks<void>): Promise<void>;
11
+ delete(key: IDBValidKey, options?: OperationCallbacks<void>): Promise<void>;
12
+ exists(key: IDBValidKey): Promise<boolean>;
13
+ query<T>(filterFn: (item: T) => boolean, options?: OperationCallbacks<T[]>): Promise<T[]>;
14
+ upsert<T>(data: T, options?: OperationCallbacks<IDBValidKey>): Promise<IDBValidKey>;
15
+ bulkInsert<T>(items: T[], options?: OperationCallbacks<IDBValidKey[]>): Promise<IDBValidKey[]>;
16
+ clear(options?: OperationCallbacks<void>): Promise<void>;
17
+ count(options?: OperationCallbacks<number>): Promise<number>;
18
+ }
19
+ export declare function createTableController(db: IDBDatabase, tableName: string): ITableController;
20
+ export declare function createTransactionTableController(tx: IDBTransaction, tableName: string): ITableController;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * IndexedDB hook system – shared types.
3
+ * @module indexedDB/types
4
+ */
5
+ /** Table schema for a single object store. */
6
+ export interface TableSchema {
7
+ /** Key path (e.g. `"id"` or `["a", "b"]`). */
8
+ keyPath: string | string[];
9
+ /** Use auto-increment primary key. */
10
+ autoIncrement?: boolean;
11
+ /** Optional index names to create (each indexes the keyPath by default). */
12
+ indexes?: string[];
13
+ }
14
+ /** Configuration passed to useIndexedDB. */
15
+ export interface IndexedDBConfig {
16
+ /** Database name. */
17
+ name: string;
18
+ /** Schema version (increment to trigger onupgradeneeded). */
19
+ version: number;
20
+ /** Map of table name → store schema (keyPath, autoIncrement, indexes). */
21
+ tables: {
22
+ [tableName: string]: TableSchema;
23
+ };
24
+ }
25
+ /** Optional callbacks for any operation. */
26
+ export interface OperationCallbacks<T = unknown> {
27
+ onSuccess?: (result: T) => void;
28
+ onError?: (error: DOMException) => void;
29
+ }
30
+ /** Options for transaction. */
31
+ export interface TransactionOptions extends OperationCallbacks<void> {
32
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Preact hook for IndexedDB: open database, create stores/indexes, return a database controller.
3
+ * Uses a singleton connection per (name, version).
4
+ * @module useIndexedDB
5
+ */
6
+ import type { IndexedDBConfig } from './indexedDB/types';
7
+ import type { IDBController } from './indexedDB/dbController';
8
+ export type { IndexedDBConfig, IDBController } from './indexedDB';
9
+ export interface UseIndexedDBReturn {
10
+ /** Database controller (table, transaction). Null until the database is open. */
11
+ db: IDBController | null;
12
+ /** True once the database is open and ready. */
13
+ isReady: boolean;
14
+ /** Error from opening the database, if any. */
15
+ error: DOMException | null;
16
+ }
17
+ /**
18
+ * Opens an IndexedDB database and returns a controller for tables and transactions.
19
+ * Handles onupgradeneeded: creates object stores and indexes from config.
20
+ * Connection is a singleton per (config.name, config.version).
21
+ *
22
+ * @param config - Database name, version, and table schemas (keyPath, autoIncrement, indexes).
23
+ * @returns { db, isReady, error }. Use db.table(name) and db.transaction(...) when isReady is true.
24
+ *
25
+ * @example
26
+ * const { db, isReady, error } = useIndexedDB({
27
+ * name: 'my-db',
28
+ * version: 1,
29
+ * tables: {
30
+ * users: { keyPath: 'id', autoIncrement: true, indexes: ['email'] },
31
+ * },
32
+ * })
33
+ * if (isReady && db) {
34
+ * const users = db.table('users')
35
+ * await users.insert({ email: 'a@b.com' })
36
+ * await db.transaction(['users'], 'readwrite', (tx) => tx.table('users').insert({ email: 'b@b.com' }))
37
+ * }
38
+ */
39
+ export declare function useIndexedDB(config: IndexedDBConfig): UseIndexedDBReturn;
@@ -0,0 +1,36 @@
1
+ export type ThreadedWorkerMode = 'sequential' | 'parallel';
2
+ export interface UseThreadedWorkerOptions {
3
+ /** Sequential: single worker, priority-ordered. Parallel: worker pool. */
4
+ mode: ThreadedWorkerMode;
5
+ /** Max concurrent workers. Only used when mode is "parallel". Default 4. */
6
+ concurrency?: number;
7
+ }
8
+ export interface RunOptions {
9
+ /** 1 = highest priority. Lower number runs first. FIFO within same priority. */
10
+ priority?: number;
11
+ }
12
+ export interface UseThreadedWorkerReturn<TData, TResult> {
13
+ /** Enqueue work. Returns a Promise that resolves with the worker result. */
14
+ run: (data: TData, options?: RunOptions) => Promise<TResult>;
15
+ /** True while any task is queued or running. */
16
+ loading: boolean;
17
+ /** Result of the most recently completed successful task. */
18
+ result: TResult | undefined;
19
+ /** Error from the most recently failed task. */
20
+ error: unknown;
21
+ /** Number of tasks currently queued + running. */
22
+ queueSize: number;
23
+ /** Clear all pending (not yet started) tasks. Running tasks continue. */
24
+ clearQueue: () => void;
25
+ /** Stop accepting new work and clear pending queue. Running tasks finish. */
26
+ terminate: () => void;
27
+ }
28
+ /**
29
+ * Production-grade hook to run async work in a queue with optional priority
30
+ * and either sequential or parallel execution.
31
+ *
32
+ * @param workerFn - Async function to run for each task (e.g. API call, heavy compute).
33
+ * @param options - mode: "sequential" | "parallel", concurrency (parallel only).
34
+ * @returns run, loading, result, error, queueSize, clearQueue, terminate.
35
+ */
36
+ export declare function useThreadedWorker<TData, TResult>(workerFn: (data: TData) => Promise<TResult>, options: UseThreadedWorkerOptions): UseThreadedWorkerReturn<TData, TResult>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * useWebRTCIP – detect local/public IPs via WebRTC ICE candidates and STUN.
3
+ * Not highly reliable; use as first-priority hint and fall back to a public IP API (e.g. ipapi.co) if needed.
4
+ * @module useWebRTCIP
5
+ */
6
+ export interface UseWebRTCIPOptions {
7
+ /** STUN server URLs (default: Google STUN). */
8
+ stunServers?: string[];
9
+ /** Stop gathering after this many ms (default: 3000). */
10
+ timeout?: number;
11
+ /** Called once per newly detected IP (no duplicates). */
12
+ onDetect?: (ip: string) => void;
13
+ }
14
+ export interface UseWebRTCIPReturn {
15
+ /** Unique IPv4 addresses found from ICE candidates. */
16
+ ips: string[];
17
+ /** True while ICE gathering is in progress. */
18
+ loading: boolean;
19
+ /** Error message if WebRTC is unavailable or detection fails. */
20
+ error: string | null;
21
+ }
22
+ /**
23
+ * Attempts to detect client IP addresses using WebRTC ICE candidates and a STUN server.
24
+ * Works frontend-only (no backend). Not guaranteed to return a public IP; use as a hint and
25
+ * fall back to a public IP API (e.g. ipapi.co, ip-api.com) if you need reliability.
26
+ *
27
+ * @param options - Optional: stunServers, timeout (ms), onDetect(ip) callback.
28
+ * @returns { ips, loading, error } – unique IPv4s, loading flag, and error message.
29
+ *
30
+ * @example
31
+ * const { ips, loading, error } = useWebRTCIP({
32
+ * timeout: 5000,
33
+ * onDetect: (ip) => console.log('Detected:', ip),
34
+ * })
35
+ * // If ips is empty and error is set, fall back to: fetch('https://api.ipify.org?format=json')
36
+ */
37
+ export declare function useWebRTCIP(options?: UseWebRTCIPOptions): UseWebRTCIPReturn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "preact-missing-hooks",
3
- "version": "1.2.0",
3
+ "version": "2.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",
@@ -10,14 +10,64 @@
10
10
  "source": "src/index.ts",
11
11
  "exports": {
12
12
  ".": {
13
+ "types": "./dist/index.d.ts",
13
14
  "import": "./dist/index.module.js",
14
- "require": "./dist/index.js",
15
- "types": "./dist/index.d.ts"
15
+ "require": "./dist/index.js"
16
16
  },
17
17
  "./useTransition": {
18
+ "types": "./dist/useTransition.d.ts",
18
19
  "import": "./dist/useTransition.module.js",
19
- "require": "./dist/useTransition.js",
20
- "types": "./dist/useTransition.d.ts"
20
+ "require": "./dist/useTransition.js"
21
+ },
22
+ "./useMutationObserver": {
23
+ "types": "./dist/useMutationObserver.d.ts",
24
+ "import": "./dist/useMutationObserver.module.js",
25
+ "require": "./dist/useMutationObserver.js"
26
+ },
27
+ "./useEventBus": {
28
+ "types": "./dist/useEventBus.d.ts",
29
+ "import": "./dist/useEventBus.module.js",
30
+ "require": "./dist/useEventBus.js"
31
+ },
32
+ "./useWrappedChildren": {
33
+ "types": "./dist/useWrappedChildren.d.ts",
34
+ "import": "./dist/useWrappedChildren.module.js",
35
+ "require": "./dist/useWrappedChildren.js"
36
+ },
37
+ "./usePreferredTheme": {
38
+ "types": "./dist/usePreferredTheme.d.ts",
39
+ "import": "./dist/usePreferredTheme.module.js",
40
+ "require": "./dist/usePreferredTheme.js"
41
+ },
42
+ "./useNetworkState": {
43
+ "types": "./dist/useNetworkState.d.ts",
44
+ "import": "./dist/useNetworkState.module.js",
45
+ "require": "./dist/useNetworkState.js"
46
+ },
47
+ "./useClipboard": {
48
+ "types": "./dist/useClipboard.d.ts",
49
+ "import": "./dist/useClipboard.module.js",
50
+ "require": "./dist/useClipboard.js"
51
+ },
52
+ "./useRageClick": {
53
+ "types": "./dist/useRageClick.d.ts",
54
+ "import": "./dist/useRageClick.module.js",
55
+ "require": "./dist/useRageClick.js"
56
+ },
57
+ "./useThreadedWorker": {
58
+ "types": "./dist/useThreadedWorker.d.ts",
59
+ "import": "./dist/useThreadedWorker.module.js",
60
+ "require": "./dist/useThreadedWorker.js"
61
+ },
62
+ "./useIndexedDB": {
63
+ "types": "./dist/useIndexedDB.d.ts",
64
+ "import": "./dist/useIndexedDB.module.js",
65
+ "require": "./dist/useIndexedDB.js"
66
+ },
67
+ "./useWebRTCIP": {
68
+ "types": "./dist/useWebRTCIP.d.ts",
69
+ "import": "./dist/useWebRTCIP.module.js",
70
+ "require": "./dist/useWebRTCIP.js"
21
71
  }
22
72
  },
23
73
  "scripts": {
@@ -56,6 +106,7 @@
56
106
  "@testing-library/jest-dom": "^6.6.3",
57
107
  "@testing-library/preact": "^3.2.4",
58
108
  "@types/jest": "^29.5.14",
109
+ "fake-indexeddb": "^6.0.2",
59
110
  "jsdom": "^26.1.0",
60
111
  "microbundle": "^0.15.1",
61
112
  "typescript": "^5.8.3",
package/src/index.ts CHANGED
@@ -5,4 +5,7 @@ export * from './useWrappedChildren'
5
5
  export * from './usePreferredTheme'
6
6
  export * from './useNetworkState'
7
7
  export * from './useClipboard'
8
- export * from './useRageClick'
8
+ export * from './useRageClick'
9
+ export * from './useThreadedWorker'
10
+ export * from './useIndexedDB'
11
+ export * from './useWebRTCIP'
@@ -0,0 +1,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 { 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
+ }
@@ -0,0 +1,11 @@
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';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Opens IndexedDB and runs onupgradeneeded to create stores and indexes.
3
+ * Singleton per (name, version).
4
+ * @module indexedDB/openDB
5
+ */
6
+
7
+ import type { IndexedDBConfig, TableSchema } from './types';
8
+ import { requestToPromise } from './requestToPromise';
9
+
10
+ const connectionCache = new Map<string, Promise<IDBDatabase>>();
11
+
12
+ /**
13
+ * Opens the database and creates/upgrades object stores and indexes from config.
14
+ * Uses a singleton cache per (name, version); repeated calls with the same config reuse the same connection.
15
+ */
16
+ export function openDB(config: IndexedDBConfig): Promise<IDBDatabase> {
17
+ const key = `${config.name}_v${config.version}`;
18
+ let promise = connectionCache.get(key);
19
+ if (promise) return promise;
20
+ promise = _openDB(config);
21
+ connectionCache.set(key, promise);
22
+ return promise;
23
+ }
24
+
25
+ function _openDB(config: IndexedDBConfig): Promise<IDBDatabase> {
26
+ return new Promise<IDBDatabase>((resolve, reject) => {
27
+ const request = indexedDB.open(config.name, config.version);
28
+ request.onerror = () => reject(request.error ?? new DOMException('Failed to open database'));
29
+ request.onsuccess = () => resolve(request.result);
30
+ request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
31
+ const db = (event.target as IDBOpenDBRequest).result;
32
+ const tables = config.tables;
33
+ for (const tableName of Object.keys(tables)) {
34
+ const schema = tables[tableName] as TableSchema;
35
+ if (!db.objectStoreNames.contains(tableName)) {
36
+ const store = db.createObjectStore(tableName, {
37
+ keyPath: schema.keyPath,
38
+ autoIncrement: schema.autoIncrement ?? false,
39
+ });
40
+ if (schema.indexes) {
41
+ for (const indexName of schema.indexes) {
42
+ store.createIndex(indexName, indexName, { unique: false });
43
+ }
44
+ }
45
+ }
46
+ }
47
+ };
48
+ });
49
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Wraps an IDBRequest in a Promise.
3
+ * @module indexedDB/requestToPromise
4
+ */
5
+
6
+ /**
7
+ * Converts an IDBRequest to a Promise. Rejects with the request's error on failure.
8
+ * @param request - Native IndexedDB request.
9
+ * @returns Promise that resolves with the request result or rejects with DOMException.
10
+ */
11
+ export function requestToPromise<T>(request: IDBRequest<T>): Promise<T> {
12
+ return new Promise<T>((resolve, reject) => {
13
+ request.onsuccess = () => resolve(request.result);
14
+ request.onerror = () => reject(request.error ?? new DOMException('Unknown IndexedDB error'));
15
+ });
16
+ }