ttrpg-tools/src/components/journal/completions.ts

83 lines
2.0 KiB
TypeScript
Raw Normal View History

/**
* Journal completions client-side loader for /__COMPLETIONS.json
*
* Fetches once on first call, caches the result. Call `invalidateCompletions()`
* to force a re-fetch (e.g., when the content source changes).
*/
import { createSignal } from "solid-js";
// ------------------- Types (mirrors CLI) -------------------
export interface DiceCompletion {
label: string;
notation: string;
source: string;
}
export interface LinkCompletion {
path: string;
label: string;
section: string | null;
}
export interface JournalCompletions {
dice: DiceCompletion[];
links: LinkCompletion[];
}
// ------------------- Cache -------------------
let _cache: JournalCompletions = { dice: [], links: [] };
let _loaded = false;
/** Thawed value signal; invalidate resets it. */
const [completions, setCompletions] = createSignal<JournalCompletions>(_cache);
// ------------------- Fetch -------------------
async function load(): Promise<void> {
try {
const resp = await fetch("/__COMPLETIONS.json");
if (resp.ok) {
const data = await resp.json();
_cache = {
dice: Array.isArray(data.dice) ? data.dice : [],
links: Array.isArray(data.links) ? data.links : [],
};
} else {
_cache = { dice: [], links: [] };
}
} catch {
_cache = { dice: [], links: [] };
}
_loaded = true;
setCompletions(_cache);
}
/** Ensure data is loaded (lazy, idempotent). */
let _promise: Promise<void> | null = null;
export function ensureCompletions(): Promise<void> {
if (!_promise) _promise = load();
return _promise;
}
/** Force a re-fetch on next use. */
export function invalidateCompletions(): void {
_loaded = false;
_promise = null;
}
// ------------------- Hook -------------------
/**
* Reactive completions data for the journal input.
* Triggers a lazy fetch on first access; returns empty arrays while loading.
*/
export function useJournalCompletions(): JournalCompletions {
if (!_loaded) {
void ensureCompletions();
}
return completions();
}