Defines the "Bulk Import All Sources" feature for the on-demand bestiary system: one-click loading of all ~104 bestiary sources with concurrent fetching, progress feedback, toast notifications, and completion reporting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4.2 KiB
Research: Bulk Import All Sources
R1: Source Code List Availability
Decision: Use the existing bestiary index's sources object keys to enumerate all source codes for bulk fetching.
Rationale: The loadBestiaryIndex() function already returns a BestiaryIndex with a sources: Record<string, string> mapping all ~104 source codes to display names. This is the single source of truth.
Alternatives considered:
- Hardcoded source list: Rejected — would drift from index and require manual maintenance.
- Fetching a remote manifest: Rejected — adds complexity and an extra network call.
R2: URL Construction Pattern
Decision: Construct fetch URLs by appending bestiary-{sourceCode.toLowerCase()}.json to a user-provided base URL, matching the existing getDefaultFetchUrl() pattern.
Rationale: The getDefaultFetchUrl helper in bestiary-index-adapter.ts already implements this pattern. The bulk import reuses it with a configurable base URL prefix.
Alternatives considered:
- Per-source URL customization: Rejected — too complex for bulk operation; single base URL is sufficient.
R3: Concurrent Fetch Strategy
Decision: Fire all fetch requests via Promise.allSettled() and let the browser handle HTTP/2 connection multiplexing and connection pooling (typically 6 concurrent connections per origin for HTTP/1.1).
Rationale: Promise.allSettled() (not Promise.all()) ensures that individual failures don't abort the entire operation. The browser naturally throttles concurrent connections, so no manual batching is needed.
Alternatives considered:
- Manual batching (e.g., 10 at a time): Rejected — adds complexity; browser pooling handles this naturally.
- Sequential fetching: Rejected — too slow for 104 sources.
R4: Progress State Management
Decision: Create a dedicated useBulkImport hook that manages import state (total, completed, failed, status) and exposes it to both the side panel component and the toast component.
Rationale: The import state needs to survive the side panel closing (toast takes over). Lifting state to a hook that lives in App.tsx ensures both UI targets can consume the same progress data.
Alternatives considered:
- Context provider: Rejected — overkill for a single piece of state consumed by 2 components.
- Global state (zustand/jotai): Rejected — project doesn't use external state management; unnecessary dependency.
R5: Toast Implementation
Decision: Build a minimal custom toast component using a React portal rendered at document.body level, positioned at bottom-center via fixed positioning.
Rationale: The spec requires no third-party toast library. A portal ensures the toast renders above all other content. The component needs only: text, progress bar, optional dismiss button, and auto-dismiss timer.
Alternatives considered:
- Third-party library (react-hot-toast, sonner): Rejected — spec explicitly requires custom component.
- Non-portal approach: Rejected — would require careful z-index management and DOM nesting.
R6: Skip-Already-Cached Strategy
Decision: Before firing fetches, check each source against isSourceCached() and build a filtered list of uncached sources. Update the total count to reflect only uncached sources.
Rationale: This avoids unnecessary network requests and gives accurate progress counts. The existing isSourceCached() function supports this directly.
Alternatives considered:
- Fetch all and overwrite: Rejected — wastes bandwidth and time.
- Check during fetch (lazy): Rejected — harder to show accurate total count upfront.
R7: Integration with Existing Bestiary Hook
Decision: The useBulkImport hook calls the existing fetchAndCacheSource from useBestiary for each source. After all sources complete, a single refreshCache() call reloads the creature map.
Rationale: Reuses the existing normalization + caching pipeline. Calling refreshCache() once at the end (instead of after each source) avoids O(N) full map rebuilds.
Alternatives considered:
- Inline the fetch/normalize/cache logic in the bulk import hook: Rejected — duplicates code.
- Call refreshCache after each source: Rejected — expensive O(N) rebuild on each call.