@cripty2001/utils 0.0.159 → 0.0.160
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.
- package/dist/react-whispr.d.ts +68 -0
- package/dist/react-whispr.js +110 -1
- package/package.json +1 -1
package/dist/react-whispr.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Whispr } from "@cripty2001/whispr";
|
|
2
|
+
import { JSONEncodable } from ".";
|
|
2
3
|
import { Dispatcher } from "./Dispatcher";
|
|
3
4
|
import { SearcherData } from "./Searcher";
|
|
4
5
|
/**
|
|
@@ -130,3 +131,70 @@ export declare function useSearcher<T>(data: SearcherData<T>[], q: string): Sear
|
|
|
130
131
|
* @returns
|
|
131
132
|
*/
|
|
132
133
|
export declare function useSafeRef<T>(value: (() => T)): T;
|
|
134
|
+
export type AsyncInputValue<C extends Record<string, JSONEncodable>, R extends Record<string, JSONEncodable>> = R & {
|
|
135
|
+
_meta: {
|
|
136
|
+
ts: number;
|
|
137
|
+
config: C;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Creates a bidirectional async input handler that manages synchronous config updates
|
|
142
|
+
* and asynchronous result computation with automatic deduplication and conflict resolution.
|
|
143
|
+
*
|
|
144
|
+
* This hook acts as a "gateway" between controlled input components and expensive async
|
|
145
|
+
* operations (like autocomplete, search, or validation). It maintains two separate concerns:
|
|
146
|
+
*
|
|
147
|
+
* 1. **External state** (`value`/`setValue` params): Contains only valid, complete data.
|
|
148
|
+
* Updates when async operations complete. Parent components see no debouncing/loading states.
|
|
149
|
+
*
|
|
150
|
+
* 2. **Internal state** (returned `value`/`setValue`): Synchronously tracks user input/config.
|
|
151
|
+
* Updates immediately on user interaction without waiting for async results.
|
|
152
|
+
*
|
|
153
|
+
* The hook automatically:
|
|
154
|
+
* - Merges updates from both external (param) and internal (returned) setValue calls
|
|
155
|
+
* - Detects and discards stale async results
|
|
156
|
+
* - Handles concurrent updates gracefully with last-write-wins semantics
|
|
157
|
+
*
|
|
158
|
+
* @template C - Configuration object type (the input/config that triggers async work)
|
|
159
|
+
* @template R - Result object type (the output of the async handler)
|
|
160
|
+
*
|
|
161
|
+
* @param value - Current external value containing both result data and metadata with config/timestamp. Metadata should be considered opaque, and always carried araoud as they are
|
|
162
|
+
* @param setValue - Callback to update external value when async operations complete
|
|
163
|
+
* @param handler - Async function that computes results from config.
|
|
164
|
+
*
|
|
165
|
+
* @returns Array containing:
|
|
166
|
+
* - `value`: Current config (updates synchronously with user input)
|
|
167
|
+
* - `setValue`: Function to update config (triggers new async operation)
|
|
168
|
+
* - `result`: Latest computed result or null if no result yet. Useful for displaying loaders
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```tsx
|
|
172
|
+
* // Parent component manages complete, valid autocomplete selections
|
|
173
|
+
* const [selectedUser, setSelectedUser] = useState<AsyncInputValue<{query: string}, {id: string, name: string}>>(...)
|
|
174
|
+
*
|
|
175
|
+
* function AutocompleteInput({selectedUser, setSelectedUser}) {
|
|
176
|
+
* const [ value, setValue, result ] = useAsyncInput<{query: string}, {id: string, name: string}>(
|
|
177
|
+
* selectedUser,
|
|
178
|
+
* setSelectedUser,
|
|
179
|
+
* async ({query}) => fetchUsers(query) // Debounced by useAsync
|
|
180
|
+
* );
|
|
181
|
+
*
|
|
182
|
+
* return (
|
|
183
|
+
* <input
|
|
184
|
+
* value={value.query}
|
|
185
|
+
* onChange={e => setValue({query: e.target.value})} // Immediate update
|
|
186
|
+
* />
|
|
187
|
+
* {result === null ? <Spinner /> : <UserList users={result} />}
|
|
188
|
+
* );
|
|
189
|
+
* }
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* @remarks
|
|
193
|
+
* The returned `result` will lag behind `value` during async processing. Consider showing a loader or some other similar indication
|
|
194
|
+
* Handler is NOT reactive. Conceptually it is a pure function that derives an async status from the value input, so there is no reason for it to be reactive, and this saves a lot heachaches with react reactivity loops.
|
|
195
|
+
*/
|
|
196
|
+
export declare function useAsyncInput<C extends Record<string, JSONEncodable>, R extends Record<string, JSONEncodable>>(value: AsyncInputValue<C, R>, setValue: (value: AsyncInputValue<C, R>) => void, handler: (config: C) => Promise<R>): [
|
|
197
|
+
value: C,
|
|
198
|
+
setValue: (value: C) => void,
|
|
199
|
+
result: R | null
|
|
200
|
+
];
|
package/dist/react-whispr.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.useAsyncEffect = useAsyncEffect;
|
|
|
11
11
|
exports.useRelTime = useRelTime;
|
|
12
12
|
exports.useSearcher = useSearcher;
|
|
13
13
|
exports.useSafeRef = useSafeRef;
|
|
14
|
+
exports.useAsyncInput = useAsyncInput;
|
|
14
15
|
const whispr_1 = require("@cripty2001/whispr");
|
|
15
16
|
const react_1 = require("react");
|
|
16
17
|
const lodash_1 = require("lodash");
|
|
@@ -108,7 +109,7 @@ function useSynced(def, value, setValue) {
|
|
|
108
109
|
const [v, setV] = (0, react_1.useState)(def);
|
|
109
110
|
if ((value !== undefined && setValue === undefined) ||
|
|
110
111
|
(value === undefined && setValue !== undefined))
|
|
111
|
-
throw new Error('Either value and setValue must be provided, or both must be undefined');
|
|
112
|
+
throw new Error('Either both value and setValue must be provided, or both must be undefined');
|
|
112
113
|
const setValueRef = (0, react_1.useRef)(setValue);
|
|
113
114
|
(0, react_1.useEffect)(() => {
|
|
114
115
|
setValueRef.current = setValue;
|
|
@@ -294,3 +295,111 @@ function useSafeRef(value) {
|
|
|
294
295
|
}
|
|
295
296
|
return ref.current;
|
|
296
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Creates a bidirectional async input handler that manages synchronous config updates
|
|
300
|
+
* and asynchronous result computation with automatic deduplication and conflict resolution.
|
|
301
|
+
*
|
|
302
|
+
* This hook acts as a "gateway" between controlled input components and expensive async
|
|
303
|
+
* operations (like autocomplete, search, or validation). It maintains two separate concerns:
|
|
304
|
+
*
|
|
305
|
+
* 1. **External state** (`value`/`setValue` params): Contains only valid, complete data.
|
|
306
|
+
* Updates when async operations complete. Parent components see no debouncing/loading states.
|
|
307
|
+
*
|
|
308
|
+
* 2. **Internal state** (returned `value`/`setValue`): Synchronously tracks user input/config.
|
|
309
|
+
* Updates immediately on user interaction without waiting for async results.
|
|
310
|
+
*
|
|
311
|
+
* The hook automatically:
|
|
312
|
+
* - Merges updates from both external (param) and internal (returned) setValue calls
|
|
313
|
+
* - Detects and discards stale async results
|
|
314
|
+
* - Handles concurrent updates gracefully with last-write-wins semantics
|
|
315
|
+
*
|
|
316
|
+
* @template C - Configuration object type (the input/config that triggers async work)
|
|
317
|
+
* @template R - Result object type (the output of the async handler)
|
|
318
|
+
*
|
|
319
|
+
* @param value - Current external value containing both result data and metadata with config/timestamp. Metadata should be considered opaque, and always carried araoud as they are
|
|
320
|
+
* @param setValue - Callback to update external value when async operations complete
|
|
321
|
+
* @param handler - Async function that computes results from config.
|
|
322
|
+
*
|
|
323
|
+
* @returns Array containing:
|
|
324
|
+
* - `value`: Current config (updates synchronously with user input)
|
|
325
|
+
* - `setValue`: Function to update config (triggers new async operation)
|
|
326
|
+
* - `result`: Latest computed result or null if no result yet. Useful for displaying loaders
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```tsx
|
|
330
|
+
* // Parent component manages complete, valid autocomplete selections
|
|
331
|
+
* const [selectedUser, setSelectedUser] = useState<AsyncInputValue<{query: string}, {id: string, name: string}>>(...)
|
|
332
|
+
*
|
|
333
|
+
* function AutocompleteInput({selectedUser, setSelectedUser}) {
|
|
334
|
+
* const [ value, setValue, result ] = useAsyncInput<{query: string}, {id: string, name: string}>(
|
|
335
|
+
* selectedUser,
|
|
336
|
+
* setSelectedUser,
|
|
337
|
+
* async ({query}) => fetchUsers(query) // Debounced by useAsync
|
|
338
|
+
* );
|
|
339
|
+
*
|
|
340
|
+
* return (
|
|
341
|
+
* <input
|
|
342
|
+
* value={value.query}
|
|
343
|
+
* onChange={e => setValue({query: e.target.value})} // Immediate update
|
|
344
|
+
* />
|
|
345
|
+
* {result === null ? <Spinner /> : <UserList users={result} />}
|
|
346
|
+
* );
|
|
347
|
+
* }
|
|
348
|
+
* ```
|
|
349
|
+
*
|
|
350
|
+
* @remarks
|
|
351
|
+
* The returned `result` will lag behind `value` during async processing. Consider showing a loader or some other similar indication
|
|
352
|
+
* Handler is NOT reactive. Conceptually it is a pure function that derives an async status from the value input, so there is no reason for it to be reactive, and this saves a lot heachaches with react reactivity loops.
|
|
353
|
+
*/
|
|
354
|
+
function useAsyncInput(value, setValue, handler) {
|
|
355
|
+
const [meta, setMeta] = (0, react_1.useState)({
|
|
356
|
+
config: value._meta.config,
|
|
357
|
+
ts: value._meta.ts
|
|
358
|
+
});
|
|
359
|
+
(0, react_1.useEffect)(() => {
|
|
360
|
+
if (value._meta.ts > meta.ts) {
|
|
361
|
+
setMeta(value._meta);
|
|
362
|
+
}
|
|
363
|
+
}, [value, meta, setMeta]);
|
|
364
|
+
// // React has always random problems with stale closures, expecially in async environment.
|
|
365
|
+
// // On the other side, the watched param of useAsync is always perfect
|
|
366
|
+
// // We aren't taking any risks, here
|
|
367
|
+
// type AsyncPack = {
|
|
368
|
+
// c: C,
|
|
369
|
+
// ts: number,
|
|
370
|
+
// }
|
|
371
|
+
// const asyncPack: AsyncPack = useMemo(() => {
|
|
372
|
+
// return {
|
|
373
|
+
// c: meta.config,
|
|
374
|
+
// ts: meta.ts,
|
|
375
|
+
// }
|
|
376
|
+
// }, [meta]);
|
|
377
|
+
const result_d = useAsync(async ({ config, ts }) => {
|
|
378
|
+
console.log("useAsyncInput", { config, ts });
|
|
379
|
+
const r = await handler(config);
|
|
380
|
+
return {
|
|
381
|
+
...r,
|
|
382
|
+
_meta: {
|
|
383
|
+
ts: ts,
|
|
384
|
+
config: config,
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}, meta, 0);
|
|
388
|
+
const result = useWhisprValue(result_d.filtered);
|
|
389
|
+
(0, react_1.useEffect)(() => {
|
|
390
|
+
if (result === null)
|
|
391
|
+
return;
|
|
392
|
+
setValue(result);
|
|
393
|
+
}, [result, setValue]);
|
|
394
|
+
const returnedSetValue = (0, react_1.useCallback)((v) => {
|
|
395
|
+
setMeta({
|
|
396
|
+
config: v,
|
|
397
|
+
ts: Date.now(),
|
|
398
|
+
});
|
|
399
|
+
}, [setMeta]);
|
|
400
|
+
return [
|
|
401
|
+
meta.config,
|
|
402
|
+
returnedSetValue,
|
|
403
|
+
result,
|
|
404
|
+
];
|
|
405
|
+
}
|
package/package.json
CHANGED