{"version":3,"file":"shard-cache.cjs","names":[],"sources":["../../src/icons/shard-cache.ts"],"sourcesContent":["import type { LucideIcon } from \"lucide-react\";\n\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { resolveIconAlias } from \"./alias\";\nimport { iconShards, type IconShard } from \"./generated/loaders\";\n\n/**\n * Slugs resolved so far, across every shard already fetched.\n *\n * Module-level and readable **synchronously**, which is the whole point: once a\n * page has rendered one icon, every other icon in that shard's range is already in\n * memory and paints on its first frame. A cache that could only be read from an\n * effect would flash a fallback for each new icon, which is the behavior\n * `lucide-react`'s own `DynamicIcon` has.\n */\nconst resolved = new Map<string, LucideIcon>();\n\n/** In-flight shard fetches, so N icons from one shard trigger one import. */\nconst inFlight = new Map<string, Promise<void>>();\n\n/**\n * Find the shard that owns a slug.\n *\n * The shards are contiguous, sorted ranges, so this is a binary search over their\n * lower bounds — 45 entries in the index rather than the 2000-entry slug→chunk map\n * that costs 120 KB in a main chunk.\n *\n * A slug sorting before the first range belongs to no shard, which is knowable\n * without any fetch: `\"0-not-an-icon\"` cannot be a lucide name.\n *\n * @param canonical - A canonical slug, aliases already resolved.\n * @returns The owning shard, or `undefined` when no range can hold it.\n */\nfunction shardFor(canonical: string): IconShard | undefined {\n    if (canonical === \"\" || canonical < iconShards[0].from) return undefined;\n\n    let low = 0;\n    let high = iconShards.length - 1;\n    while (low < high) {\n        const mid = Math.ceil((low + high) / 2);\n        if (iconShards[mid].from <= canonical) low = mid;\n        else high = mid - 1;\n    }\n    return iconShards[low];\n}\n\n/**\n * Shard ids whose chunk actually arrived.\n *\n * Needed to tell \"still loading\" from \"no such icon\": until the shard arrives,\n * absence from `resolved` proves nothing. Only once the shard is in here does a\n * missing slug mean the name is genuinely wrong — which is what lets `Icon` warn\n * about a typo without also warning on every icon mid-flight.\n *\n * A shard that *failed* deliberately does not land here. Collapsing \"the chunk\n * 404'd\" into \"no such icon\" is what made a stale deploy look like a typo.\n */\nconst loadedShards = new Set<string>();\n\n/**\n * Shards whose fetch gave up, and when.\n *\n * The timestamp is what stops a permanent fallback: `useIcon` re-runs its effect\n * on every miss, so a shard is retried on a later render — after a cooldown, so a\n * chunk that is genuinely gone cannot turn into a request loop.\n */\nconst failedShards = new Map<string, number>();\n\n/**\n * Backoff before each retry, in milliseconds.\n *\n * Two retries, both fast, because the failure this exists for is a transient one:\n * a flaky connection, or a chunk whose CDN edge has not caught up. A 404 from a\n * deploy that rotated hashed filenames is *not* fixed by retrying — for that, the\n * error subscription is the signal, and the app reloads.\n */\nconst RETRY_DELAYS = [100, 400] as const;\n\n/** How long a failed shard is left alone before another render may retry it. */\nconst RETRY_COOLDOWN_MS = 10_000;\n\n/** What a shard failure reports to whoever subscribed. */\nexport interface IconLoadError {\n    /** The shard key whose chunk could not be fetched. */\n    shard: string;\n    /** The slug whose render asked for it. */\n    slug: string;\n    /** Attempts made before giving up, retries included. */\n    attempts: number;\n    /** The last rejection from the dynamic import. */\n    error: unknown;\n}\n\n/** Callbacks to run when a shard fetch gives up. */\nconst errorListeners = new Set<(detail: IconLoadError) => void>();\n\n/** Callbacks to run after a shard lands, so mounted `<Icon>`s re-render. */\nconst listeners = new Set<() => void>();\n\n/**\n * Make icon components resolvable by slug, with no provider and no plugin.\n *\n * Call it once from an entrypoint. The icons land in the same table a fetched\n * shard fills, so every `<Icon>` below — including one that renders before the\n * call, since registering notifies subscribers — resolves them synchronously and\n * never issues a request.\n *\n * This is the whole setup for a closed catalog: an admin panel with twenty known\n * icons pays two lines instead of a build plugin plus a provider. `IconProvider`\n * stays for what is genuinely tree-scoped — the `size`/`strokeWidth` defaults and\n * a registry that must override the global one for one subtree.\n *\n * A slug lucide does not ship is registered as-is, which is how an app adds its\n * own artwork to the same `<Icon name>` call site.\n *\n * @example\n * import { registerIcons } from \"tempest-react-sdk/icons\";\n * import { Save, Trash2 } from \"lucide-react\";\n *\n * registerIcons({ save: Save, \"trash-2\": Trash2 });\n *\n * @param icons - Slug → icon component. Deprecated slugs are stored under the\n *   canonical name, so both spellings resolve.\n */\nexport function registerIcons(icons: Readonly<Record<string, LucideIcon>>): void {\n    let changed = false;\n    for (const [slug, icon] of Object.entries(icons)) {\n        const canonical = resolveIconAlias(slug);\n        if (resolved.get(canonical) === icon) continue;\n        resolved.set(canonical, icon);\n        changed = true;\n    }\n    if (!changed) return;\n    for (const listener of listeners) listener();\n}\n\n/**\n * Read an icon out of the cache without triggering a fetch.\n *\n * @param slug - Any icon slug, canonical or deprecated.\n * @returns The icon component, or `undefined` when its shard is not loaded yet.\n */\nexport function peekIcon(slug: string): LucideIcon | undefined {\n    return resolved.get(resolveIconAlias(slug));\n}\n\n/**\n * Load the shard that owns `slug`, if it is not loaded or loading already.\n *\n * Resolves without error for an unknown slug: a name that came from an API is\n * data, not a programming mistake, so the caller renders its fallback instead of\n * seeing an exception thrown mid-render.\n *\n * @param slug - Any icon slug, canonical or deprecated.\n * @returns A promise that settles once the shard is in the cache.\n */\nexport function loadIcon(slug: string): Promise<void> {\n    const canonical = resolveIconAlias(slug);\n    if (resolved.has(canonical)) return Promise.resolve();\n\n    const shard = shardFor(canonical);\n    if (!shard) return Promise.resolve();\n\n    const pending = inFlight.get(shard.id);\n    if (pending) return pending;\n\n    const failedAt = failedShards.get(shard.id);\n    if (failedAt !== undefined && performance.now() - failedAt < RETRY_COOLDOWN_MS) {\n        return Promise.resolve();\n    }\n\n    const promise = fetchShard(shard, canonical).finally(() => {\n        inFlight.delete(shard.id);\n        for (const listener of listeners) listener();\n    });\n    inFlight.set(shard.id, promise);\n    return promise;\n}\n\n/**\n * Fetch one shard, retrying a couple of times before giving up.\n *\n * Never rejects: an icon whose chunk did not arrive is a rendering outcome, not a\n * programming mistake, so the caller keeps showing its fallback. What it does\n * instead of swallowing the failure is record it — so `iconStatus` can say\n * `\"error\"` rather than lying with `\"missing\"` — and report it to whoever\n * subscribed.\n *\n * @param shard - The shard being fetched.\n * @param slug - The canonical slug whose render asked for it, for the report.\n * @returns A promise that settles once the attempts are done.\n */\nasync function fetchShard(shard: IconShard, slug: string): Promise<void> {\n    let lastError: unknown;\n\n    for (let attempt = 0; attempt <= RETRY_DELAYS.length; attempt += 1) {\n        try {\n            const mod = await shard.load();\n            for (const [name, icon] of Object.entries(mod.default)) resolved.set(name, icon);\n            loadedShards.add(shard.id);\n            failedShards.delete(shard.id);\n            return;\n        } catch (error) {\n            lastError = error;\n            const backoff = RETRY_DELAYS[attempt];\n            if (backoff === undefined) break;\n            await sleep(backoff);\n        }\n    }\n\n    failedShards.set(shard.id, performance.now());\n    reportIconLoadError({\n        shard: shard.id,\n        slug,\n        attempts: RETRY_DELAYS.length + 1,\n        error: lastError,\n    });\n}\n\n/**\n * Wait, as a promise.\n *\n * @param ms - Delay in milliseconds.\n * @returns A promise resolved after the delay.\n */\nfunction sleep(ms: number): Promise<void> {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Hand a shard failure to whoever is listening.\n *\n * With no subscriber the failure would be silent, which is the state this\n * replaces — so in a dev build it warns instead. An app that wired the\n * subscription up owns the reporting from then on and gets no console noise.\n *\n * @param detail - What failed.\n */\nfunction reportIconLoadError(detail: IconLoadError): void {\n    if (errorListeners.size === 0) {\n        if (isDevBuild()) warnShardFailure(detail);\n        return;\n    }\n    for (const listener of errorListeners) listener(detail);\n}\n\nconst warnedShards = new Set<string>();\n\n/**\n * Warn once per shard that failed to load.\n *\n * Once, because every `<Icon>` under that letter re-triggers the path and a\n * warning per icon would bury the console.\n *\n * @param detail - What failed.\n */\nfunction warnShardFailure({ shard, slug, attempts }: IconLoadError): void {\n    if (warnedShards.has(shard)) return;\n    warnedShards.add(shard);\n    console.warn(\n        `[tempest-react-sdk] icon shard \"${shard}\" failed to load after ${attempts} attempts ` +\n            `(first asked for by \"${slug}\"). The usual cause is a deploy that rotated hashed ` +\n            `chunk names while this tab was open. Subscribe with \\`subscribeToIconErrors\\` to ` +\n            `report it and reload.`,\n    );\n}\n\n/**\n * Subscribe to shard fetches that gave up.\n *\n * The failure is routine in a long-lived tab: a deploy rotates hashed chunk\n * names, the old asset leaves the CDN, and the next icon that needs it 404s. That\n * used to end in a permanent fallback with no signal — nothing for the user,\n * nothing for observability. Subscribing is what turns it into both.\n *\n * @example\n * import { subscribeToIconErrors } from \"tempest-react-sdk/icons\";\n *\n * subscribeToIconErrors(({ shard, error }) => {\n *     Sentry.captureException(error, { tags: { iconShard: shard } });\n *     promptReloadForStaleChunks();\n * });\n *\n * @param listener - Called once per shard that gave up.\n * @returns An unsubscribe function.\n */\nexport function subscribeToIconErrors(listener: (detail: IconLoadError) => void): () => void {\n    errorListeners.add(listener);\n    return () => errorListeners.delete(listener);\n}\n\n/**\n * Whether a slug is ready, loading, unknown, or unreachable.\n *\n * `\"missing\"` is only reported once that is actually knowable: either lucide has\n * no icons under that initial letter, or the letter's shard **arrived** without\n * the slug in it. A shard that failed to load reports `\"error\"` instead — the two\n * used to collapse into `\"missing\"`, which turned a stale deploy into what looked\n * like a typo and made `Icon` warn about a name that is perfectly valid.\n *\n * @param slug - Any icon slug, canonical or deprecated.\n * @returns The slug's resolution state.\n */\nexport function iconStatus(slug: string): \"ready\" | \"loading\" | \"missing\" | \"error\" {\n    const canonical = resolveIconAlias(slug);\n    if (resolved.has(canonical)) return \"ready\";\n    const shard = shardFor(canonical);\n    if (!shard) return \"missing\";\n    if (loadedShards.has(shard.id)) return \"missing\";\n    return failedShards.has(shard.id) ? \"error\" : \"loading\";\n}\n\n/**\n * Warm the cache for slugs you know you are about to render.\n *\n * Useful right before opening a menu or a picker: the shards land while the user\n * is still reaching for it, so nothing flashes a fallback.\n *\n * @param slugs - Icon slugs to preload.\n * @returns A promise that settles when every needed shard is in the cache.\n */\nexport async function preloadIcons(slugs: readonly string[]): Promise<void> {\n    await Promise.all(slugs.map(loadIcon));\n}\n\n/**\n * Subscribe to shard arrivals.\n *\n * @param listener - Called after each shard lands.\n * @returns An unsubscribe function.\n */\nexport function subscribeToIcons(listener: () => void): () => void {\n    listeners.add(listener);\n    return () => listeners.delete(listener);\n}\n"],"mappings":"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"}