use-weather-forecast 0.1.128 → 0.1.129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -12
- package/dist/api/forecast.d.ts +24 -0
- package/dist/api/geolocation.d.ts +39 -9
- package/dist/api/http.d.ts +42 -12
- package/dist/api/providers/index.d.ts +21 -0
- package/dist/api/providers/met-no.d.ts +11 -0
- package/dist/api/providers/open-meteo.d.ts +26 -0
- package/dist/api/providers/types.d.ts +30 -0
- package/dist/api/providers/wttr.d.ts +11 -0
- package/dist/hooks/useWeatherForecast.d.ts +7 -0
- package/dist/index.cjs +812 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +786 -131
- package/dist/index.js.map +1 -1
- package/dist/lib/cache.d.ts +9 -0
- package/dist/lib/time.d.ts +11 -0
- package/dist/lib/units.d.ts +16 -0
- package/dist/lib/validate.d.ts +55 -0
- package/dist/types.d.ts +37 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://codecov.io/gh/OpenSourceAGI/qwksearch-research-agent)
|
|
4
4
|
|
|
5
|
-
React weather forecast component using Open-Meteo for current, hourly, and daily forecasts and Cloudflare/ipapi.co for IP geolocation.
|
|
5
|
+
React weather forecast component using Open-Meteo for current, hourly, and daily forecasts and Cloudflare/ipapi.co for IP geolocation, with a fallback chain behind both.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- Current weather.
|
|
10
10
|
- Next hours forecast.
|
|
11
11
|
- Next days forecast.
|
|
12
|
-
- IP geolocation
|
|
12
|
+
- Four weather upstreams tried in order, four IP geolocation upstreams behind that.
|
|
13
|
+
- Every request validated before it is sent, retried with backoff when it can recover.
|
|
14
|
+
- Stale-cache fallback so a total outage still renders a widget.
|
|
13
15
|
- Latitude/longitude override.
|
|
14
16
|
- Split SVG weather icon components.
|
|
15
17
|
- TypeScript + tsup library scaffold.
|
|
@@ -37,6 +39,66 @@ export default function App() {
|
|
|
37
39
|
}
|
|
38
40
|
```
|
|
39
41
|
|
|
42
|
+
## Reliability
|
|
43
|
+
|
|
44
|
+
Nothing here needs configuring -- these are the defaults -- but this is what
|
|
45
|
+
the package does when an upstream misbehaves.
|
|
46
|
+
|
|
47
|
+
### `Weather request failed: 400 Bad Request`
|
|
48
|
+
|
|
49
|
+
Open-Meteo answers `400` for a query it cannot parse, and the usual way to
|
|
50
|
+
build one is not a typo in your props: an IP geolocation upstream answers
|
|
51
|
+
`200` with no coordinates in the body, `Number(undefined)` becomes `NaN`,
|
|
52
|
+
`latitude=NaN` goes out on the wire. Everything that goes into a request is
|
|
53
|
+
validated first now:
|
|
54
|
+
|
|
55
|
+
| Input | What happens to it |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `latitude` / `longitude` | Must be finite and on the globe, and are rounded to 4 decimals. A geolocation response without them counts as a failed lookup, so the next provider gets a turn. Unusable props fall back to IP geolocation. |
|
|
58
|
+
| `location.timezone` | Canonicalized (`US/Pacific` becomes `America/Los_Angeles`); a zone the runtime cannot resolve is dropped rather than sent, and Open-Meteo resolves the zone from the coordinates instead. |
|
|
59
|
+
| `forecastDays` / `forecastHours` | Clamped to the 1-16 days and 1-384 hours the API accepts. |
|
|
60
|
+
| Variable list | A `400` from an endpoint that refused one of the optional variables is retried once with the minimal set the widget needs. |
|
|
61
|
+
|
|
62
|
+
### Retries
|
|
63
|
+
|
|
64
|
+
Requests go through [`grab-url`](https://www.npmjs.com/package/grab-url) rather
|
|
65
|
+
than `fetch`. Each one is tried up to `retryAttempts` times (3 by default for a
|
|
66
|
+
forecast, 2 per geolocation provider since that chain is longer) with
|
|
67
|
+
exponential backoff from `retryDelay` (400ms, then 800ms, ...). A `429`, a
|
|
68
|
+
`5xx`, a timeout or a dropped connection is repeated; a request the server
|
|
69
|
+
called invalid (`400`, `404`) is not, since replaying it only burns the
|
|
70
|
+
upstream's rate limit.
|
|
71
|
+
|
|
72
|
+
### Fallback APIs
|
|
73
|
+
|
|
74
|
+
| Order | Weather | IP geolocation |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| 1 | `open-meteo` -- `api.open-meteo.com/v1/forecast` | `geoEndpoint` (the bundled worker), when given |
|
|
77
|
+
| 2 | `open-meteo-gfs` -- the GFS model endpoint | `ipapi` -- ipapi.co |
|
|
78
|
+
| 3 | `met-no` -- met.no, a different operator and model | `ipwho` -- ipwho.is |
|
|
79
|
+
| 4 | `wttr` -- wttr.in, no key required | `geojs` -- get.geojs.io |
|
|
80
|
+
| 5 | | `freeipapi` -- freeipapi.com |
|
|
81
|
+
|
|
82
|
+
The fallbacks normalize their own condition codes, units and timestamps into
|
|
83
|
+
the same shape Open-Meteo returns, so a failover is invisible in the rendered
|
|
84
|
+
widget (wttr.in is 3-hourly rather than hourly). If every provider fails, a
|
|
85
|
+
cached forecast up to a day old is served instead of throwing; only if there is
|
|
86
|
+
no cache either does an error reach the widget, listing what each provider
|
|
87
|
+
said.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<WeatherForecast
|
|
91
|
+
weatherProviders={['open-meteo', 'met-no']}
|
|
92
|
+
geoProviders={['ipwho', 'geojs']}
|
|
93
|
+
retryAttempts={3}
|
|
94
|
+
retryDelay={400}
|
|
95
|
+
timeout={15}
|
|
96
|
+
fallbackLocation={{ city: 'Austin', latitude: 30.27, longitude: -97.74 }}
|
|
97
|
+
allowStaleCache
|
|
98
|
+
onProviderError={({ provider, stage, error }) => console.warn(stage, provider, error.message)}
|
|
99
|
+
/>
|
|
100
|
+
```
|
|
101
|
+
|
|
40
102
|
## Direct API usage
|
|
41
103
|
|
|
42
104
|
```ts
|
|
@@ -72,10 +134,17 @@ This package no longer uses ipinfo.io. Instead:
|
|
|
72
134
|
no mixed-content issues, though ipapi.co's free tier is rate-limited (1,000
|
|
73
135
|
requests/day) — deploy the worker and pass `geoEndpoint` for higher-volume or
|
|
74
136
|
production use.
|
|
75
|
-
- A failed lookup is **repeated** before
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
137
|
+
- A failed lookup is **repeated** before the chain moves on: `getClientLocation`
|
|
138
|
+
tries each provider twice by default, waiting 500ms, then walks the rest of the
|
|
139
|
+
chain (ipapi.co, ipwho.is, get.geojs.io, freeipapi.com) so a single rate-limited
|
|
140
|
+
or cold-start response doesn't take the whole forecast down. Tune it with the
|
|
141
|
+
third argument: `getClientLocation(geoEndpoint, ip, { attempts: 5, retryDelay: 250, providers: ['ipwho'] })`.
|
|
142
|
+
- A response **without usable coordinates counts as a failure** rather than being
|
|
143
|
+
passed on as `NaN`, and `fallbackLocation` covers the case where every provider
|
|
144
|
+
is down.
|
|
145
|
+
- The bundled worker does the same on its side: it falls back to the IP lookups
|
|
146
|
+
when Cloudflare's own geolocation carries no coordinates, and answers `502` with
|
|
147
|
+
a reason instead of a body the caller cannot use.
|
|
79
148
|
|
|
80
149
|
### Deploying the geo worker
|
|
81
150
|
|
|
@@ -90,16 +159,20 @@ This deploys `worker/geo-worker.ts` via Wrangler. Use the resulting `*.workers.d
|
|
|
90
159
|
## Caching
|
|
91
160
|
|
|
92
161
|
`getWeatherForecast` caches each response in `localStorage` for 30 minutes, keyed by
|
|
93
|
-
the
|
|
162
|
+
the request itself (location + units + forecast range + timezone), so a fallback
|
|
163
|
+
provider's answer is reused exactly like the primary one's. Repeated calls for the same
|
|
94
164
|
location/options within that window are served from the cache instead of hitting
|
|
95
|
-
Open-Meteo again, which keeps the widget well under Open-Meteo's rate limits.
|
|
96
|
-
|
|
97
|
-
|
|
165
|
+
Open-Meteo again, which keeps the widget well under Open-Meteo's rate limits. Past
|
|
166
|
+
the 30 minutes an entry is kept for a day as the last-resort fallback described
|
|
167
|
+
above, and evicted after that. Call `clearWeatherForecastCache()` to evict
|
|
168
|
+
everything (e.g. in tests). The cache is a no-op in non-browser environments (SSR)
|
|
169
|
+
or when `localStorage` is unavailable/full.
|
|
98
170
|
|
|
99
171
|
## Notes
|
|
100
172
|
|
|
101
|
-
- Open-Meteo powers the forecast data.
|
|
102
|
-
- Cloudflare's `request.cf
|
|
173
|
+
- Open-Meteo powers the forecast data, with met.no and wttr.in behind it.
|
|
174
|
+
- Cloudflare's `request.cf`, ipapi.co, ipwho.is, get.geojs.io and freeipapi.com
|
|
175
|
+
power IP geolocation (see above).
|
|
103
176
|
- HTTP requests go through [`grab-url`](https://www.npmjs.com/package/grab-url), the
|
|
104
177
|
repo-wide client, rather than raw `fetch`. It is the package's only runtime
|
|
105
178
|
dependency.
|
package/dist/api/forecast.d.ts
CHANGED
|
@@ -1,2 +1,26 @@
|
|
|
1
1
|
import type { WeatherForecastData, WeatherForecastOptions } from '../types';
|
|
2
|
+
import { type ForecastRequest } from './providers';
|
|
3
|
+
/**
|
|
4
|
+
* Everything that goes on the wire is validated first, then the providers are
|
|
5
|
+
* tried in order until one answers.
|
|
6
|
+
*
|
|
7
|
+
* The `400 Bad Request` this used to render came from an unvalidated request:
|
|
8
|
+
* a geolocation upstream answering 200 with no coordinates put `latitude=NaN`
|
|
9
|
+
* in the URL, and an unknown timezone string or an out-of-range `forecastDays`
|
|
10
|
+
* did the same. Those are caught in {@link buildForecastRequest} now, before a
|
|
11
|
+
* request is sent, and anything that still fails falls through to the next
|
|
12
|
+
* provider and finally to a stale cache entry.
|
|
13
|
+
*/
|
|
14
|
+
/** Cache key for a request, so a fallback provider's answer is reused like the primary one's. */
|
|
15
|
+
export declare function forecastCacheKey(request: ForecastRequest): string;
|
|
16
|
+
/** Validate and clamp every option into a request that no upstream can reject as malformed. */
|
|
17
|
+
export declare function buildForecastRequest(options?: WeatherForecastOptions): Promise<ForecastRequest>;
|
|
18
|
+
/**
|
|
19
|
+
* Current, hourly and daily forecast for a location.
|
|
20
|
+
*
|
|
21
|
+
* @param options Location, units, range, provider chain and retry settings.
|
|
22
|
+
* @returns The first provider's answer, a cached one, or -- if every provider
|
|
23
|
+
* failed and `allowStaleCache` is on -- the last good answer for this exact
|
|
24
|
+
* request.
|
|
25
|
+
*/
|
|
2
26
|
export declare function getWeatherForecast(options?: WeatherForecastOptions): Promise<WeatherForecastData>;
|
|
@@ -1,20 +1,50 @@
|
|
|
1
1
|
import type { WeatherLocation } from '../types';
|
|
2
|
+
/** One IP geolocation upstream. */
|
|
3
|
+
export type GeoProvider = {
|
|
4
|
+
id: string;
|
|
5
|
+
/** Used in the thrown message, e.g. `ipapi.co lookup`. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Full URL for the lookup; `ip` is the address to look up, if any. */
|
|
8
|
+
url: (ip?: string) => string;
|
|
9
|
+
/** Map the body onto a location, or return `null` if it carries none. */
|
|
10
|
+
parse: (body: Record<string, any>) => Partial<WeatherLocation> | null;
|
|
11
|
+
};
|
|
2
12
|
export type GeolocationOptions = {
|
|
3
|
-
/** default=
|
|
13
|
+
/** default=2 How many times each provider is tried before the next one is used. */
|
|
4
14
|
attempts?: number;
|
|
5
|
-
/** default=500 Milliseconds
|
|
15
|
+
/** default=500 Milliseconds before a retry; each further wait doubles. */
|
|
6
16
|
retryDelay?: number;
|
|
17
|
+
/** default=8 Seconds before a single lookup is aborted. */
|
|
18
|
+
timeout?: number;
|
|
19
|
+
/** Providers to try, by id or as objects. Omit for {@link DEFAULT_GEO_PROVIDERS}. */
|
|
20
|
+
providers?: (string | GeoProvider)[];
|
|
21
|
+
/** Used when every provider failed, instead of throwing. */
|
|
22
|
+
fallbackLocation?: Partial<WeatherLocation> | null;
|
|
23
|
+
/** Called for each provider that failed, before the next one is tried. */
|
|
24
|
+
onProviderError?: (info: {
|
|
25
|
+
provider: string;
|
|
26
|
+
error: Error;
|
|
27
|
+
}) => void;
|
|
7
28
|
};
|
|
29
|
+
export declare const ipapiProvider: GeoProvider;
|
|
30
|
+
export declare const ipwhoProvider: GeoProvider;
|
|
31
|
+
export declare const geojsProvider: GeoProvider;
|
|
32
|
+
export declare const freeipapiProvider: GeoProvider;
|
|
33
|
+
/** Tried in order; each is a different operator, so a rate limit on one is not a rate limit on all. */
|
|
34
|
+
export declare const DEFAULT_GEO_PROVIDERS: GeoProvider[];
|
|
35
|
+
/** The bundled Cloudflare worker, wrapped as a provider so it joins the same chain. */
|
|
36
|
+
export declare function createWorkerGeoProvider(geoEndpoint: string): GeoProvider;
|
|
37
|
+
export declare function resolveGeoProviders(providers?: (string | GeoProvider)[], geoEndpoint?: string): GeoProvider[];
|
|
8
38
|
/**
|
|
9
|
-
* Resolve the caller's location
|
|
10
|
-
*
|
|
39
|
+
* Resolve the caller's location from a deployed geo worker or from one of the
|
|
40
|
+
* public IP lookups.
|
|
11
41
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
42
|
+
* Each provider is retried with a growing delay, then the chain moves on; only
|
|
43
|
+
* if all of them fail (and no `fallbackLocation` was given) does an error
|
|
44
|
+
* reach the caller, listing what every provider said.
|
|
15
45
|
*
|
|
16
|
-
* @param geoEndpoint URL of a deployed geo worker;
|
|
46
|
+
* @param geoEndpoint URL of a deployed geo worker; it is tried before the public providers.
|
|
17
47
|
* @param ip Look up this IP instead of the caller's own.
|
|
18
|
-
* @param options
|
|
48
|
+
* @param options Retry, timeout, provider chain and fallback settings.
|
|
19
49
|
*/
|
|
20
50
|
export declare function getClientLocation(geoEndpoint?: string, ip?: string, options?: GeolocationOptions): Promise<WeatherLocation>;
|
package/dist/api/http.d.ts
CHANGED
|
@@ -1,25 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared JSON transport for
|
|
3
|
-
* and
|
|
2
|
+
* Shared JSON transport for every upstream this package talks to (the geo
|
|
3
|
+
* worker, the IP geolocation providers and the weather providers).
|
|
4
4
|
*
|
|
5
|
-
* `grab-url` is the HTTP client used across this repo
|
|
6
|
-
* applies a timeout and can repeat a failed request itself.
|
|
7
|
-
* resolves rather than rejects when a request fails, handing
|
|
8
|
-
* on `.error`, so every call site has to inspect the payload
|
|
9
|
-
* lives here so the callers can just `await` a typed body.
|
|
5
|
+
* `grab-url` is the HTTP client used across this repo rather than `fetch`: it
|
|
6
|
+
* parses the JSON, applies a timeout and can repeat a failed request itself.
|
|
7
|
+
* Unlike `fetch` it resolves rather than rejects when a request fails, handing
|
|
8
|
+
* back the message on `.error`, so every call site has to inspect the payload
|
|
9
|
+
* -- that check lives here so the callers can just `await` a typed body.
|
|
10
|
+
*
|
|
11
|
+
* On top of that this module adds what the upstreams actually need to stay up:
|
|
12
|
+
* a classified retry loop (repeat what can recover, give up immediately on a
|
|
13
|
+
* request the server called invalid) with exponential backoff.
|
|
10
14
|
*/
|
|
11
15
|
export type GrabJsonOptions = {
|
|
12
16
|
headers?: Record<string, string>;
|
|
13
|
-
/** default=
|
|
14
|
-
|
|
15
|
-
/** default=
|
|
17
|
+
/** default=3 Total tries for this request, including the first one. */
|
|
18
|
+
attempts?: number;
|
|
19
|
+
/** default=400 Milliseconds before the second try; each further wait doubles. */
|
|
20
|
+
retryDelay?: number;
|
|
21
|
+
/** default=15 Seconds before a single try is aborted. */
|
|
16
22
|
timeout?: number;
|
|
23
|
+
/**
|
|
24
|
+
* default=0 Times grab-url itself repeats a try that failed at the transport
|
|
25
|
+
* level, inside each of our `attempts`. Left at 0 so the backoff and the
|
|
26
|
+
* status classification below stay in charge of every repeat.
|
|
27
|
+
*/
|
|
28
|
+
retryAttempts?: number;
|
|
29
|
+
/**
|
|
30
|
+
* default=false Repeat even when the server rejected the request as invalid
|
|
31
|
+
* (a 4xx other than 408/429). Off by default: replaying a request that is
|
|
32
|
+
* malformed only wastes the upstream's rate limit.
|
|
33
|
+
*/
|
|
34
|
+
retryClientErrors?: boolean;
|
|
17
35
|
};
|
|
36
|
+
/** A failed upstream call, carrying the HTTP status when the failure had one. */
|
|
37
|
+
export declare class HttpRequestError extends Error {
|
|
38
|
+
readonly status?: number;
|
|
39
|
+
readonly retryable: boolean;
|
|
40
|
+
constructor(message: string, options?: {
|
|
41
|
+
status?: number;
|
|
42
|
+
retryable?: boolean;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** No status at all means the request never reached the server -- worth a repeat. */
|
|
46
|
+
export declare function isRetryableStatus(status?: number): boolean;
|
|
18
47
|
/**
|
|
19
|
-
* Request a JSON body,
|
|
48
|
+
* Request a JSON body, repeating what can recover and throwing a labelled
|
|
49
|
+
* {@link HttpRequestError} once it cannot.
|
|
20
50
|
*
|
|
21
51
|
* @param url Full URL to request.
|
|
22
52
|
* @param label Prefix for the thrown message, e.g. `ipapi.co lookup`.
|
|
23
|
-
* @param options Transport
|
|
53
|
+
* @param options Transport and retry options.
|
|
24
54
|
*/
|
|
25
55
|
export declare function grabJson<T>(url: string, label: string, options?: GrabJsonOptions): Promise<T>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WeatherProvider } from './types';
|
|
2
|
+
export * from './types';
|
|
3
|
+
export { createOpenMeteoProvider, openMeteoProvider, openMeteoGfsProvider } from './open-meteo';
|
|
4
|
+
export { metNoProvider, symbolToWmoCode } from './met-no';
|
|
5
|
+
export { wttrProvider, wwoToWmoCode } from './wttr';
|
|
6
|
+
/**
|
|
7
|
+
* Tried in order until one answers.
|
|
8
|
+
*
|
|
9
|
+
* Open-Meteo stays the primary source; its GFS endpoint covers a query the
|
|
10
|
+
* default model refused or rate-limited, and met.no and wttr.in are run by
|
|
11
|
+
* different operators entirely, so they still answer when Open-Meteo itself is
|
|
12
|
+
* unreachable.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_WEATHER_PROVIDERS: WeatherProvider[];
|
|
15
|
+
/**
|
|
16
|
+
* Turn the `weatherProviders` option into the list to try.
|
|
17
|
+
*
|
|
18
|
+
* @param providers Ids (`'open-meteo'`, `'met-no'`, `'wttr'`, `'open-meteo-gfs'`)
|
|
19
|
+
* or provider objects of your own. Omit for {@link DEFAULT_WEATHER_PROVIDERS}.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveWeatherProviders(providers?: (string | WeatherProvider)[]): WeatherProvider[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WeatherProvider } from './types';
|
|
2
|
+
export declare function symbolToWmoCode(symbol: string | undefined): number;
|
|
3
|
+
/**
|
|
4
|
+
* met.no is an independent upstream (a different operator, a different model),
|
|
5
|
+
* so it stays useful exactly when Open-Meteo is the thing that is down.
|
|
6
|
+
*
|
|
7
|
+
* It reports instants in UTC with Celsius and m/s; everything is converted to
|
|
8
|
+
* the request's units and rewritten into the naive local timestamps the widget
|
|
9
|
+
* renders, so a failover is invisible.
|
|
10
|
+
*/
|
|
11
|
+
export declare const metNoProvider: WeatherProvider;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ForecastRequest, WeatherProvider } from './types';
|
|
2
|
+
/** Every variable the widget can render. */
|
|
3
|
+
declare const FULL_VARIABLES: {
|
|
4
|
+
current: string[];
|
|
5
|
+
hourly: string[];
|
|
6
|
+
daily: string[];
|
|
7
|
+
};
|
|
8
|
+
type Variables = typeof FULL_VARIABLES;
|
|
9
|
+
export declare function buildOpenMeteoUrl(endpoint: string, request: ForecastRequest, variables?: Variables): string;
|
|
10
|
+
/**
|
|
11
|
+
* Open-Meteo, the package's primary source.
|
|
12
|
+
*
|
|
13
|
+
* @param id Provider id, e.g. `open-meteo`.
|
|
14
|
+
* @param label Name used in the aggregated error message.
|
|
15
|
+
* @param endpoint Full endpoint URL; the model endpoints (`/v1/gfs` and
|
|
16
|
+
* friends) take the same query as `/v1/forecast`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createOpenMeteoProvider(id: string, label: string, endpoint: string): WeatherProvider;
|
|
19
|
+
/** `https://api.open-meteo.com/v1/forecast` -- the default, best-model source. */
|
|
20
|
+
export declare const openMeteoProvider: WeatherProvider;
|
|
21
|
+
/**
|
|
22
|
+
* The GFS model endpoint on the same host. It answers when `/v1/forecast` is
|
|
23
|
+
* rate-limited or refuses the query, and speaks exactly the same dialect.
|
|
24
|
+
*/
|
|
25
|
+
export declare const openMeteoGfsProvider: WeatherProvider;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { GrabJsonOptions } from '../http';
|
|
2
|
+
import type { TemperatureUnit, WeatherForecastData, WeatherLocation, WindSpeedUnit } from '../../types';
|
|
3
|
+
/** A forecast request after validation: every field is safe to put in a URL. */
|
|
4
|
+
export type ForecastRequest = {
|
|
5
|
+
location: WeatherLocation;
|
|
6
|
+
temperatureUnit: TemperatureUnit;
|
|
7
|
+
windSpeedUnit: WindSpeedUnit;
|
|
8
|
+
/** 1-16. */
|
|
9
|
+
forecastDays: number;
|
|
10
|
+
/** 1-384. */
|
|
11
|
+
forecastHours: number;
|
|
12
|
+
/** A validated IANA zone, or `undefined` to let the provider resolve it. */
|
|
13
|
+
timezone?: string;
|
|
14
|
+
/** Transport and retry settings passed through to grab-url. */
|
|
15
|
+
transport: GrabJsonOptions;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* One upstream that can answer a {@link ForecastRequest}.
|
|
19
|
+
*
|
|
20
|
+
* Providers are tried in order and the first one to answer wins, so each has
|
|
21
|
+
* to return the same normalized shape whatever units or codes it speaks
|
|
22
|
+
* natively.
|
|
23
|
+
*/
|
|
24
|
+
export type WeatherProvider = {
|
|
25
|
+
/** Stable id, e.g. `open-meteo`. Used in options and in the error message. */
|
|
26
|
+
id: string;
|
|
27
|
+
/** Human-readable name for the aggregated error message. */
|
|
28
|
+
label: string;
|
|
29
|
+
fetchForecast: (request: ForecastRequest) => Promise<WeatherForecastData>;
|
|
30
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WeatherProvider } from './types';
|
|
2
|
+
export declare function wwoToWmoCode(code: unknown): number;
|
|
3
|
+
/**
|
|
4
|
+
* wttr.in, the second independent fallback.
|
|
5
|
+
*
|
|
6
|
+
* Its free endpoint needs no key and answers with the location's own local
|
|
7
|
+
* times, in both Celsius and Fahrenheit, so only the wind speed has to be
|
|
8
|
+
* converted. It is 3-hourly rather than hourly -- a coarser forecast, but a
|
|
9
|
+
* rendered widget rather than an error message.
|
|
10
|
+
*/
|
|
11
|
+
export declare const wttrProvider: WeatherProvider;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { WeatherForecastData, WeatherForecastOptions } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Fetch a forecast for `options`, refetching whenever they change.
|
|
4
|
+
*
|
|
5
|
+
* The last good forecast is kept while a refetch is in flight *and* if that
|
|
6
|
+
* refetch fails, so a rate-limited upstream leaves the widget showing the
|
|
7
|
+
* previous data rather than replacing it with an error.
|
|
8
|
+
*/
|
|
2
9
|
export declare function useWeatherForecast(options?: WeatherForecastOptions): {
|
|
3
10
|
data: WeatherForecastData | null;
|
|
4
11
|
error: Error | null;
|