Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | /**
* Dropdown registry — on-demand load and cache of named dropdown lists.
*
* Widgets declare their dropdown dependency by name:
* widget: {type: 'dropdown', dropdown: 'marine.species'}
*
* When a component needs the data, it calls:
* dropdownRegistry.get('marine.species', loader)
*
* The loader is responsible for calling the backend (e.g. via useHandler).
* The registry deduplicates concurrent requests and caches the result.
*
* The naming convention for the backend handler is:
* {subject}.dropdown.list({name: 'marine.species'}) → [{value, label}]
*/
import type {IDropdownOption} from '@feasibleone/blong';
type LoaderFn = (name: string) => Promise<IDropdownOption[]>;
type BatchLoaderFn = (names: string[]) => Promise<Record<string, IDropdownOption[]>>;
class DropdownRegistry {
private cache = new Map<string, IDropdownOption[]>();
private pending = new Map<string, Promise<IDropdownOption[]>>();
/**
* Get a dropdown by name, loading it via the provided loader if not cached.
*
* @param name - Dropdown key, e.g. 'marine.species'
* @param loader - Function that fetches the dropdown data from the backend
*/
async get(name: string, loader: LoaderFn): Promise<IDropdownOption[]> {
if (this.cache.has(name)) return this.cache.get(name)!;
if (!this.pending.has(name)) {
const promise = loader(name)
.then(data => {
this.cache.set(name, data);
this.pending.delete(name);
return data;
})
.catch(err => {
this.pending.delete(name);
// Re-throw so callers (e.g. DropdownWidget) can handle the error
// (show a dialog, log the user out, etc.).
throw err;
});
this.pending.set(name, promise);
}
return this.pending.get(name)!;
}
/**
* Pre-populate the cache without async loading (used by mocks and preload).
*/
set(name: string, data: IDropdownOption[]): void {
this.cache.set(name, data);
}
/**
* Batch-preload multiple dropdowns. Names already cached are skipped.
*
* @param names - Dropdown names to load
* @param loader - Batch loader that returns a map of name → options
*/
preload(names: string[], loader: BatchLoaderFn): Promise<void> {
const missing = names.filter(n => !this.cache.has(n) && !this.pending.has(n));
if (!missing.length) return Promise.resolve();
const promise = loader(missing).then(results => {
for (const [name, data] of Object.entries(results)) {
this.cache.set(name, data);
this.pending.delete(name);
}
});
// Mark all as pending immediately to avoid duplicate requests
for (const name of missing) {
this.pending.set(
name,
promise.then(() => this.cache.get(name) ?? []),
);
}
return promise.then(() => undefined);
}
/** Return true if the named dropdown is already loaded */
has(name: string): boolean {
return this.cache.has(name);
}
/** Clear the cache (useful in tests) */
clear(): void {
this.cache.clear();
this.pending.clear();
}
}
/** Singleton dropdown registry — shared across all components */
export const dropdownRegistry = new DropdownRegistry();
|