1- // GP base resolution for worker vs. worker-less (static snapshot) deployments.
1+ // GpSource — the single place that knows where GP data comes from. Hides the
2+ // worker probe, its memoization, the two URL schemes, and the API→static
3+ // fallback behind three fetch-shaped functions: fetchGpIndex, fetchGpGroup,
4+ // and fetchGpMetadata.
25//
3- // On the first call we probe `/api/groups.json`. If a worker answers with a
6+ // On first use we probe `/api/groups.json`. If a worker answers with a
47// parseable JSON body we use the API base `/api/gp/` and keep the returned
58// group index (names + counts); otherwise we fall back to the static snapshot
69// under `data/gp/` (written by `pnpm update-gp`, served as part of the static
710// build) and read its `index.json` instead. The probe runs once per session
8- // (memoized promise).
11+ // (memoized promise). When the worker API fails for a single group
12+ // mid-session, that request is retried once against the static snapshot.
913
1014const API_BASE = "/api/gp/" ;
1115const STATIC_BASE = "data/gp/" ;
@@ -23,7 +27,7 @@ export interface GpIndexEntry {
2327 count ?: number ;
2428}
2529
26- export interface GpSourceInfo {
30+ interface GpSourceInfo {
2731 base : string ;
2832 index : GpIndexEntry [ ] ;
2933}
@@ -68,26 +72,16 @@ async function probeGpSource(): Promise<GpSourceInfo> {
6872 }
6973}
7074
71- // Base plus the group index (names + counts) from the same probe request.
72- export function resolveGpSource ( ) : Promise < GpSourceInfo > {
75+ function resolveGpSource ( ) : Promise < GpSourceInfo > {
7376 infoPromise ??= probeGpSource ( ) ;
7477 return infoPromise ;
7578}
7679
77- export async function resolveGpBase ( ) : Promise < string > {
78- return ( await resolveGpSource ( ) ) . base ;
79- }
80-
81- // For tests: reset the memoized probe.
82- export function resetGpBase ( ) : void {
83- infoPromise = undefined ;
84- }
85-
8680// Resolve a preset source into a fetchable URL. Bare group names
8781// (^[a-zA-Z0-9_-]+$) are resolved against the probed base; anything containing
8882// "/" or "." (legacy .txt URLs, absolute/relative paths) passes through
8983// unchanged so it can still be parsed via payload sniffing.
90- export function resolveGroupUrl ( source : string , base : string ) : string {
84+ function resolveGroupUrl ( source : string , base : string ) : string {
9185 if ( / ^ [ a - z A - Z 0 - 9 _ - ] + $ / . test ( source ) ) {
9286 return `${ base } ${ source } .json` ;
9387 }
@@ -97,17 +91,62 @@ export function resolveGroupUrl(source: string, base: string): string {
9791// Static-snapshot URL for a bare group name, used as a per-request fallback
9892// when the worker API fails mid-session. Explicit URL sources have no static
9993// counterpart and return undefined.
100- export function staticGroupUrl ( source : string ) : string | undefined {
94+ function staticGroupUrl ( source : string ) : string | undefined {
10195 if ( / ^ [ a - z A - Z 0 - 9 _ - ] + $ / . test ( source ) ) {
10296 return `${ STATIC_BASE } ${ source } .json` ;
10397 }
10498 return undefined ;
10599}
106100
107- // The metadata endpoint for the resolved base: the worker serves it at
108- // `/api/metadata.json`; the static snapshot writes `data/gp/metadata.json` .
109- export function resolveMetadataUrl ( base : string ) : string {
110- return base === API_BASE ? API_METADATA_URL : STATIC_METADATA_URL ;
101+ // The group index (names + counts) from the probe. Best-effort: empty when
102+ // neither the worker nor the static snapshot answers; never rejects .
103+ export async function fetchGpIndex ( ) : Promise < GpIndexEntry [ ] > {
104+ return ( await resolveGpSource ( ) ) . index ;
111105}
112106
113- export { API_BASE , STATIC_BASE } ;
107+ // The payload text for a preset source. Bare group names resolve against the
108+ // probed base; when the worker API fails mid-session (network error or
109+ // non-2xx), the request is retried once against the static snapshot bundled
110+ // with the build. Explicit URL sources pass through without a fallback.
111+ export async function fetchGpGroup ( source : string ) : Promise < string > {
112+ const { base } = await resolveGpSource ( ) ;
113+ const url = resolveGroupUrl ( source , base ) ;
114+ try {
115+ // Plain fetch (NOT mode:"no-cors") — the API is same-origin; an opaque
116+ // response would have an unreadable body and break parsing.
117+ const response = await fetch ( url ) ;
118+ if ( ! response . ok ) {
119+ throw new Error ( response . statusText ) ;
120+ }
121+ return await response . text ( ) ;
122+ } catch ( error ) {
123+ const fallback = staticGroupUrl ( source ) ;
124+ if ( fallback === undefined || fallback === url ) {
125+ throw error ;
126+ }
127+ console . log ( `GP fetch failed for ${ url } , retrying static snapshot ${ fallback } ` , error ) ;
128+ const response = await fetch ( fallback ) ;
129+ if ( ! response . ok ) {
130+ throw new Error ( response . statusText , { cause : error } ) ;
131+ }
132+ return await response . text ( ) ;
133+ }
134+ }
135+
136+ // Parsed remote metadata rules from the endpoint matching the probed base:
137+ // the worker serves `/api/metadata.json`; the static snapshot writes
138+ // `data/gp/metadata.json`. Throws on failure — the caller decides tolerance.
139+ export async function fetchGpMetadata ( ) : Promise < unknown > {
140+ const { base } = await resolveGpSource ( ) ;
141+ const url = base === API_BASE ? API_METADATA_URL : STATIC_METADATA_URL ;
142+ const response = await fetch ( url ) ;
143+ if ( ! response . ok ) {
144+ throw new Error ( response . statusText ) ;
145+ }
146+ return await response . json ( ) ;
147+ }
148+
149+ // For tests: reset the memoized probe.
150+ export function resetGpSource ( ) : void {
151+ infoPromise = undefined ;
152+ }
0 commit comments