@mailwoman/bdc 8.3.0
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 +7 -0
- package/index.ts +2 -0
- package/out/index.d.ts +3 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +3 -0
- package/out/index.js.map +1 -0
- package/out/schema.d.ts +138 -0
- package/out/schema.d.ts.map +1 -0
- package/out/schema.js +88 -0
- package/out/schema.js.map +1 -0
- package/out/sdk/build-bdc.d.ts +214 -0
- package/out/sdk/build-bdc.d.ts.map +1 -0
- package/out/sdk/build-bdc.js +500 -0
- package/out/sdk/build-bdc.js.map +1 -0
- package/out/sdk/buildings.d.ts +84 -0
- package/out/sdk/buildings.d.ts.map +1 -0
- package/out/sdk/buildings.js +75 -0
- package/out/sdk/buildings.js.map +1 -0
- package/out/sdk/client.d.ts +237 -0
- package/out/sdk/client.d.ts.map +1 -0
- package/out/sdk/client.js +430 -0
- package/out/sdk/client.js.map +1 -0
- package/out/sdk/common.d.ts +279 -0
- package/out/sdk/common.d.ts.map +1 -0
- package/out/sdk/common.js +162 -0
- package/out/sdk/common.js.map +1 -0
- package/out/sdk/data-collection.d.ts +244 -0
- package/out/sdk/data-collection.d.ts.map +1 -0
- package/out/sdk/data-collection.js +50 -0
- package/out/sdk/data-collection.js.map +1 -0
- package/out/sdk/download.d.ts +36 -0
- package/out/sdk/download.d.ts.map +1 -0
- package/out/sdk/download.js +76 -0
- package/out/sdk/download.js.map +1 -0
- package/out/sdk/filing-dates.d.ts +57 -0
- package/out/sdk/filing-dates.d.ts.map +1 -0
- package/out/sdk/filing-dates.js +50 -0
- package/out/sdk/filing-dates.js.map +1 -0
- package/out/sdk/filing-landscape.d.ts +124 -0
- package/out/sdk/filing-landscape.d.ts.map +1 -0
- package/out/sdk/filing-landscape.js +216 -0
- package/out/sdk/filing-landscape.js.map +1 -0
- package/out/sdk/index.d.ts +15 -0
- package/out/sdk/index.d.ts.map +1 -0
- package/out/sdk/index.js +15 -0
- package/out/sdk/index.js.map +1 -0
- package/out/sdk/list-files.d.ts +39 -0
- package/out/sdk/list-files.d.ts.map +1 -0
- package/out/sdk/list-files.js +26 -0
- package/out/sdk/list-files.js.map +1 -0
- package/out/sdk/location.d.ts +40 -0
- package/out/sdk/location.d.ts.map +1 -0
- package/out/sdk/location.js +23 -0
- package/out/sdk/location.js.map +1 -0
- package/out/sdk/nearest-infrastructure.d.ts +84 -0
- package/out/sdk/nearest-infrastructure.d.ts.map +1 -0
- package/out/sdk/nearest-infrastructure.js +84 -0
- package/out/sdk/nearest-infrastructure.js.map +1 -0
- package/out/sdk/parsing.d.ts +57 -0
- package/out/sdk/parsing.d.ts.map +1 -0
- package/out/sdk/parsing.js +87 -0
- package/out/sdk/parsing.js.map +1 -0
- package/out/sdk/plausibility.d.ts +256 -0
- package/out/sdk/plausibility.d.ts.map +1 -0
- package/out/sdk/plausibility.js +334 -0
- package/out/sdk/plausibility.js.map +1 -0
- package/out/sdk/technologies.d.ts +229 -0
- package/out/sdk/technologies.d.ts.map +1 -0
- package/out/sdk/technologies.js +217 -0
- package/out/sdk/technologies.js.map +1 -0
- package/package.json +97 -0
- package/schema.ts +172 -0
- package/sdk/build-bdc.ts +810 -0
- package/sdk/buildings.ts +87 -0
- package/sdk/client.ts +643 -0
- package/sdk/common.ts +363 -0
- package/sdk/data-collection.ts +268 -0
- package/sdk/download.ts +91 -0
- package/sdk/filing-dates.ts +88 -0
- package/sdk/filing-landscape.ts +287 -0
- package/sdk/index.ts +14 -0
- package/sdk/list-files.ts +72 -0
- package/sdk/location.ts +45 -0
- package/sdk/nearest-infrastructure.ts +129 -0
- package/sdk/parsing.ts +124 -0
- package/sdk/plausibility.ts +512 -0
- package/sdk/technologies.ts +258 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software.
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file FCC Broadband Data Collection (BDC) public-API client, built on {@linkcode APIClient}.
|
|
6
|
+
*
|
|
7
|
+
* Originally re-homed from Nexus's `sync/fcc/bdc/client.ts` (relicense-by-copy, no provenance
|
|
8
|
+
* headers) as a raw-`fetch` factory. That version had no throttle, no cache and no retry at all, and
|
|
9
|
+
* threw a bespoke `Error` a caller could only branch on by reading its prose. All four now come from
|
|
10
|
+
* `@mailwoman/core/api`, the repo's default base for HTTP clients (see `AGENTS.md`).
|
|
11
|
+
* `filer/sdk/sec-client.ts` is the worked example this follows. What stays BDC-specific:
|
|
12
|
+
*
|
|
13
|
+
* 1. The credential fail-fast off `$private.FCC_MAP_USERNAME`/`FCC_MAP_API_KEY`. A
|
|
14
|
+
* silently-unauthenticated client just 401s on first use, which is a worse failure mode than
|
|
15
|
+
* failing at construction.
|
|
16
|
+
* 2. The `username` + `hash_value` PLAIN HEADER PAIR — read carefully off the Nexus original's
|
|
17
|
+
* `axios.headers` config, this is NOT bearer or basic auth.
|
|
18
|
+
* 3. The request budget: {@linkcode BDC_DEFAULT_REQUESTS_PER_MINUTE} requests per MINUTE, six
|
|
19
|
+
* seconds apart. See that constant for the sourcing and for why the interval gate is set too.
|
|
20
|
+
* 4. UN-UNWRAPPED response bodies. Every BDC endpoint nests its payload under a `data` key
|
|
21
|
+
* (`{ data: [...] }`), and callers pluck `.data` themselves at the call site — `filing-dates.ts`
|
|
22
|
+
* and `list-files.ts` both do. {@linkcode BDCClient.get} deliberately does NOT unwrap, so the
|
|
23
|
+
* envelope stays visible in the caller's own response type.
|
|
24
|
+
* 5. The zip path ({@linkcode BDCClient.getArrayBuffer}), which is binary and UNCACHED. See its
|
|
25
|
+
* docstring.
|
|
26
|
+
*
|
|
27
|
+
* ERROR CONTRACT. Every failure past construction is a {@linkcode ResourceError}, so an ingestion run
|
|
28
|
+
* branches on `status` plus {@linkcode isTransientResourceError} and never on message prose:
|
|
29
|
+
*
|
|
30
|
+
* | Outcome | Caller action | Test |
|
|
31
|
+
* | --------------------------------- | ---------------- | ------------------------------------------ |
|
|
32
|
+
* | 401 / 403 | abort the run | `error.status === 401 \|\| === 403` |
|
|
33
|
+
* | 404 | skip this file | `error.status === 404` |
|
|
34
|
+
* | exhausted 429/5xx | requeue | `isTransientResourceError(error)` |
|
|
35
|
+
* | exhausted network/timeout | requeue | `isTransientResourceError(error)` |
|
|
36
|
+
* | undecodable body | programmer bug | `isTransientResourceError(error)` is false |
|
|
37
|
+
*/
|
|
38
|
+
import { APIClient, systemClock } from "@mailwoman/core/api";
|
|
39
|
+
import { buildDiskStorage } from "@mailwoman/core/api/disk-storage";
|
|
40
|
+
import { $private } from "@mailwoman/core/env";
|
|
41
|
+
import { ResourceError } from "@mailwoman/core/errors";
|
|
42
|
+
import { dataRootPath } from "@mailwoman/core/utils";
|
|
43
|
+
// Re-exported so a caller branching on this client's failures needs exactly one import.
|
|
44
|
+
export { isTransientResourceError } from "@mailwoman/core/api";
|
|
45
|
+
export { ResourceError } from "@mailwoman/core/errors";
|
|
46
|
+
/**
|
|
47
|
+
* The FCC BDC public-API base URL every request is resolved against.
|
|
48
|
+
*/
|
|
49
|
+
export const BDC_API_BASE_URL = "https://broadbandmap.fcc.gov/api/public";
|
|
50
|
+
/**
|
|
51
|
+
* The FCC's published request ceiling for the Broadband Map public API: **10 requests per MINUTE**, i.e. one every six
|
|
52
|
+
* seconds. Sixty times tighter than SEC EDGAR's per-second cap, so it is the dominant cost of any BDC ingestion run.
|
|
53
|
+
*
|
|
54
|
+
* SOURCING, stated precisely because it could not be verified from here: this figure comes from the operator's reading
|
|
55
|
+
* of the FCC's own API documentation. It was NOT confirmed against a fetchable source — the API spec is a Box-hosted
|
|
56
|
+
* PDF, and `broadbandmap.fcc.gov/api-documentation` does not resolve. Treat it as the published limit as reported, not
|
|
57
|
+
* as something this repo checked.
|
|
58
|
+
*
|
|
59
|
+
* This is the DEFAULT, not a clamp. `createSECClient` clamps because SEC's limit is verifiable, actively policed, and
|
|
60
|
+
* published in fetchable HTML; none of that holds here, so pinning an unverified number as law would be false
|
|
61
|
+
* precision. {@linkcode CreateBDCClientOptions.requestsPerMinute} tunes it in either direction, and the throttle meter
|
|
62
|
+
* (see {@linkcode BDCClient.throttleStats}) is how a real run reports what the setting actually cost.
|
|
63
|
+
*
|
|
64
|
+
* If FCC ever answers with a 429, drop this to 9 before anything else: pacing exactly AT a published rate leaves no
|
|
65
|
+
* headroom for event-loop jitter, and a grant that lands a millisecond late shifts into the following window. See
|
|
66
|
+
* `core/api/pacer.ts`'s real-clock caveat, and `SEC_DEFAULT_REQUESTS_PER_SECOND` for the same decision taken under a
|
|
67
|
+
* measurement.
|
|
68
|
+
*/
|
|
69
|
+
export const BDC_DEFAULT_REQUESTS_PER_MINUTE = 10;
|
|
70
|
+
/**
|
|
71
|
+
* Milliseconds in a minute — the numerator when turning a requests/minute budget into a pacing interval.
|
|
72
|
+
*/
|
|
73
|
+
const MS_PER_MINUTE = 60_000;
|
|
74
|
+
/**
|
|
75
|
+
* Divisors and the percentage scale {@linkcode formatBDCThrottleStats} renders through.
|
|
76
|
+
*/
|
|
77
|
+
const MS_PER_SECOND = 1000;
|
|
78
|
+
const SECONDS_PER_MINUTE = 60;
|
|
79
|
+
const PERCENT = 100;
|
|
80
|
+
/**
|
|
81
|
+
* How long a cached BDC JSON response stays fresh by default.
|
|
82
|
+
*
|
|
83
|
+
* 24h, chosen against the FILING CADENCE rather than a wall-clock intuition. `listAsOfDates` gains an entry when FCC
|
|
84
|
+
* publishes a new BDC vintage — twice a year (a June 30 and a December 31 `as_of_date`) — and `listAvailabilityData`
|
|
85
|
+
* gains entries when a provider refiles inside an existing vintage, which happens in bursts over the weeks after a
|
|
86
|
+
* vintage drops. Neither moves hour to hour, so a shorter TTL buys nothing except six seconds of throttle per repeat
|
|
87
|
+
* call: at 10 requests/minute every cache hit is worth six seconds, and a `gazetteer build bdc` re-run over a handful
|
|
88
|
+
* of states re-asks the same two endpoints many times.
|
|
89
|
+
*
|
|
90
|
+
* Not longer, either: the whole point of re-reading `listAsOfDates` is to notice a new vintage, and an entry that
|
|
91
|
+
* outlived the day it was written would silently hide one — the same failure mode `createSECClient`'s mutable-endpoint
|
|
92
|
+
* TTL exists to prevent. A day is far inside the weeks-long window in which anyone acts on a new filing.
|
|
93
|
+
*
|
|
94
|
+
* There is no immutable-forever class here the way `/Archives/` is for SEC. Every BDC JSON endpoint this client reaches
|
|
95
|
+
* is an index that can gain a revision, so ONE TTL covers all of them.
|
|
96
|
+
*/
|
|
97
|
+
const DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
98
|
+
/**
|
|
99
|
+
* Total attempts (including the first) before giving up on a 429/5xx or a network-class failure.
|
|
100
|
+
*/
|
|
101
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
102
|
+
/**
|
|
103
|
+
* Base delay for the exponential backoff between retry attempts, in milliseconds.
|
|
104
|
+
*/
|
|
105
|
+
const DEFAULT_BASE_RETRY_DELAY_MS = 500;
|
|
106
|
+
/**
|
|
107
|
+
* Per-attempt socket-inactivity timeout for a JSON request, in milliseconds.
|
|
108
|
+
*/
|
|
109
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
110
|
+
/**
|
|
111
|
+
* Per-attempt socket-inactivity timeout for a zip download, in milliseconds — deliberately far longer than the JSON
|
|
112
|
+
* one. A BDC availability archive is routinely hundreds of megabytes, and Axios applies `timeout` through
|
|
113
|
+
* `req.setTimeout`, i.e. an idle-socket timer rather than a total-elapsed budget, so this bounds "the transfer stalled"
|
|
114
|
+
* without capping how long a large but healthy download may run.
|
|
115
|
+
*/
|
|
116
|
+
const DEFAULT_DOWNLOAD_TIMEOUT_MS = 300_000;
|
|
117
|
+
/**
|
|
118
|
+
* The status {@linkcode toArrayBuffer} reports a non-binary body under — a client misconfiguration, not an upstream
|
|
119
|
+
* failure, and mapped as a `payload` kind so `isTransientResourceError` reads it as terminal.
|
|
120
|
+
*/
|
|
121
|
+
const HTTP_INTERNAL_SERVER_ERROR = 500;
|
|
122
|
+
/**
|
|
123
|
+
* The two statuses that mean "these credentials were not accepted". Both abort a run rather than skipping one file, and
|
|
124
|
+
* both get the explanation in {@linkcode explainCredentialFailure}.
|
|
125
|
+
*/
|
|
126
|
+
const HTTP_UNAUTHORIZED = 401;
|
|
127
|
+
const HTTP_FORBIDDEN = 403;
|
|
128
|
+
/**
|
|
129
|
+
* Build the absolute request URL for `path`, with `params` as its query string.
|
|
130
|
+
*
|
|
131
|
+
* `path` is APPENDED to {@linkcode BDC_API_BASE_URL}, never RESOLVED against it. That distinction is load-bearing: `new
|
|
132
|
+
* URL("https://elsewhere.example/x", BDC_API_BASE_URL)` would resolve to `elsewhere.example` and carry the credential
|
|
133
|
+
* header pair there, while string concatenation can only ever produce a path under the BDC origin.
|
|
134
|
+
*/
|
|
135
|
+
function buildBDCURL(path, params = {}) {
|
|
136
|
+
const url = new URL(`${BDC_API_BASE_URL}${path}`);
|
|
137
|
+
for (const [key, value] of Object.entries(params)) {
|
|
138
|
+
if (value !== undefined) {
|
|
139
|
+
url.searchParams.set(key, String(value));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return url;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Rewrite a credential rejection into an error that names the cause. A bare "401 Unauthorized" from an FCC endpoint
|
|
146
|
+
* reads as "the resource is missing" or "we're blocked", and this project has already lost a debugging cycle to exactly
|
|
147
|
+
* that on a generic FCC 403. The status and URN are reconstructed identically, so a caller's `status === 401` branch is
|
|
148
|
+
* unaffected; anything that is not a credential rejection is rethrown untouched.
|
|
149
|
+
*/
|
|
150
|
+
function explainCredentialFailure(error, url, username) {
|
|
151
|
+
if (!(error instanceof ResourceError))
|
|
152
|
+
return error;
|
|
153
|
+
if (error.status !== HTTP_UNAUTHORIZED && error.status !== HTTP_FORBIDDEN)
|
|
154
|
+
return error;
|
|
155
|
+
const explained = ResourceError.from(error.status, `FCC BDC request failed: ${error.status} (${url}). The BDC public API rejected the credential pair, which it ` +
|
|
156
|
+
`takes as the plain \`username\` + \`hash_value\` request headers (NOT bearer or basic auth). The configured ` +
|
|
157
|
+
`username was "${username}"; check it and \`FCC_MAP_API_KEY\` against the account registered at ` +
|
|
158
|
+
`https://broadbandmap.fcc.gov. This is not retried — retrying a rejected credential cannot succeed, and at ` +
|
|
159
|
+
`10 requests/minute it would burn six seconds per attempt.`, "axios", "response", error.status === HTTP_UNAUTHORIZED ? "unauthorized" : "forbidden");
|
|
160
|
+
explained.cause = error;
|
|
161
|
+
return explained;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Coerce a binary response body into an `ArrayBuffer`.
|
|
165
|
+
*
|
|
166
|
+
* Axios's Node adapter hands back a `Buffer` for `responseType: "arraybuffer"` (it only stringifies for every OTHER
|
|
167
|
+
* response type), while its fetch adapter hands back a real `ArrayBuffer` — so both shapes have to be accepted, and the
|
|
168
|
+
* `Buffer` one is what production actually sees. The zero-copy branch matters: these bodies are hundreds of megabytes,
|
|
169
|
+
* and `Buffer.concat` allocates a dedicated, exactly-sized backing store for anything past Node's small-buffer pool, so
|
|
170
|
+
* the view spans its whole `ArrayBuffer` and can be handed over as-is.
|
|
171
|
+
*/
|
|
172
|
+
function toArrayBuffer(data) {
|
|
173
|
+
if (data instanceof ArrayBuffer)
|
|
174
|
+
return data;
|
|
175
|
+
if (ArrayBuffer.isView(data)) {
|
|
176
|
+
const { buffer, byteOffset, byteLength } = data;
|
|
177
|
+
if (byteOffset === 0 && byteLength === buffer.byteLength)
|
|
178
|
+
return buffer;
|
|
179
|
+
return buffer.slice(byteOffset, byteOffset + byteLength);
|
|
180
|
+
}
|
|
181
|
+
throw ResourceError.from(HTTP_INTERNAL_SERVER_ERROR, `FCC BDC download returned a ${typeof data} body where binary bytes were expected. This is a client ` +
|
|
182
|
+
'misconfiguration (the request must carry `responseType: "arraybuffer"`), not an upstream failure.', "axios", "payload", "not-binary");
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* A constructed FCC BDC public-API client. Build one with {@linkcode createBDCClient}, which resolves the credentials,
|
|
186
|
+
* the throttle, the cache, and every default.
|
|
187
|
+
*/
|
|
188
|
+
export class BDCClient extends APIClient {
|
|
189
|
+
/**
|
|
190
|
+
* Issue an authenticated `GET` against the BDC public API and parse the JSON response body, subject to the on-disk
|
|
191
|
+
* cache, the request throttle, and bounded retry.
|
|
192
|
+
*
|
|
193
|
+
* `path` is appended to {@linkcode BDC_API_BASE_URL} as-is (a leading slash, e.g. `/map/listAsOfDates`). `params`
|
|
194
|
+
* become the request's query string; the response body is returned UN-unwrapped — every BDC endpoint nests its
|
|
195
|
+
* payload under a `data` key (`{ data: [...] }`), so callers pluck `.data` themselves at the call site.
|
|
196
|
+
*
|
|
197
|
+
* Concurrent calls for the same URL that both miss the cache share a single in-flight request — the cache
|
|
198
|
+
* interceptor's own stampede guard, which the raw-`fetch` client had no equivalent of.
|
|
199
|
+
*/
|
|
200
|
+
async get(path, params, options = {}) {
|
|
201
|
+
const url = buildBDCURL(path, params);
|
|
202
|
+
const config = options.skipCache
|
|
203
|
+
? { url: url.toString(), cache: false }
|
|
204
|
+
: { url: url.toString() };
|
|
205
|
+
return this.#request(config, url);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Issue an authenticated `GET` and return the raw response body as an `ArrayBuffer` — the binary counterpart to
|
|
209
|
+
* {@linkcode BDCClient.get}. Used for the zip-wrapped availability downloads (`downloadBDCFile` in `./download.ts`),
|
|
210
|
+
* where the response is a `.zip` archive rather than a JSON envelope.
|
|
211
|
+
*
|
|
212
|
+
* CACHING IS OFF FOR THIS PATH, deliberately, and it is not a tunable. The response cache validates and persists
|
|
213
|
+
* JSON: it would reject a zip on the way in (the validator requires a `data`-keyed object), and even if it accepted
|
|
214
|
+
* one, serializing a multi-hundred-megabyte archive through `JSON.stringify` into a hash-named file would be a second
|
|
215
|
+
* copy of a thing `downloadBDCFile` already writes to disk itself — which is also where its real cache check lives
|
|
216
|
+
* (it returns the extracted CSV's path without issuing any request when that file exists). Two disk copies of the
|
|
217
|
+
* same archive, one of them unreadable.
|
|
218
|
+
*
|
|
219
|
+
* The throttle still applies: skipping the cache is not a way around the rate budget.
|
|
220
|
+
*/
|
|
221
|
+
async getArrayBuffer(path, params) {
|
|
222
|
+
const url = buildBDCURL(path, params);
|
|
223
|
+
const config = {
|
|
224
|
+
url: url.toString(),
|
|
225
|
+
responseType: "arraybuffer",
|
|
226
|
+
timeout: this.config.downloadTimeoutMs,
|
|
227
|
+
cache: false,
|
|
228
|
+
};
|
|
229
|
+
return toArrayBuffer(await this.#request(config, url));
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* What this client has spent waiting on the throttle so far. At six seconds a grant, a bulk ingest is throttle-bound
|
|
233
|
+
* by construction, and this is the measurement to assess a rate change against — see
|
|
234
|
+
* {@linkcode formatBDCThrottleStats} for the one-line rendering `gazetteer build bdc` prints.
|
|
235
|
+
*/
|
|
236
|
+
throttleStats() {
|
|
237
|
+
return this.config.readThrottleStats();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Issue one request and return its body, with a credential rejection explained. Shared by both public methods so the
|
|
241
|
+
* explanation cannot drift between the JSON and binary paths.
|
|
242
|
+
*/
|
|
243
|
+
async #request(config, url) {
|
|
244
|
+
try {
|
|
245
|
+
const response = await this.fetch(config);
|
|
246
|
+
return response.data;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
throw explainCredentialFailure(error, url, this.config.username);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Render {@linkcode BDCThrottleStats} as one human-readable line, for the end of an ingestion run.
|
|
255
|
+
*/
|
|
256
|
+
export function formatBDCThrottleStats(stats) {
|
|
257
|
+
const share = stats.elapsedMs > 0 ? Math.round((stats.waitingMs / stats.elapsedMs) * PERCENT) : 0;
|
|
258
|
+
return (`elapsed ${formatDuration(stats.elapsedMs)} · ${formatDuration(stats.waitingMs)} (${share}%) spent waiting on ` +
|
|
259
|
+
`the request throttle across ${stats.waits} wait(s) and ${stats.cooldowns} budget cooldown(s)`);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* `123456` → `"2m 3s"`. Whole seconds only: nothing this measures is sub-second.
|
|
263
|
+
*/
|
|
264
|
+
function formatDuration(ms) {
|
|
265
|
+
const totalSeconds = Math.round(ms / MS_PER_SECOND);
|
|
266
|
+
const minutes = Math.floor(totalSeconds / SECONDS_PER_MINUTE);
|
|
267
|
+
const seconds = totalSeconds % SECONDS_PER_MINUTE;
|
|
268
|
+
return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* A {@linkcode ClockLike} that records how long the client spends asleep, plus the reader that snapshots it.
|
|
272
|
+
*
|
|
273
|
+
* The clock is the only seam `APIClient` exposes that every wait passes through — the pacer sleeps on it, the cooldown
|
|
274
|
+
* timer sleeps on it, and the retry backoff sleeps on it — so wrapping it is how the waiting becomes visible without
|
|
275
|
+
* touching `core/api`.
|
|
276
|
+
*
|
|
277
|
+
* WAITS ARE UNIONED, NOT SUMMED, and that is the whole subtlety here. Under a concurrent fan-out every caller sleeps at
|
|
278
|
+
* once, and each one's wait is longer than the last: 40 concurrent requests at a 6 s interval sleep 6 s, 12 s, … 234 s,
|
|
279
|
+
* which SUMS to 78 minutes of "waiting" inside a run that took 3m54s — measured, and reported as `2000%` by the first
|
|
280
|
+
* version of this meter. Tracking the depth of in-flight sleeps and charging only the wall-clock span during which at
|
|
281
|
+
* least one was outstanding answers the question actually being asked: how much of the elapsed time went to the
|
|
282
|
+
* throttle rather than to transferring. For the serial ingest `gazetteer build bdc` actually performs the two are
|
|
283
|
+
* identical.
|
|
284
|
+
*/
|
|
285
|
+
function createMeteredClock(base) {
|
|
286
|
+
const startedAt = base.now();
|
|
287
|
+
let waitingMs = 0;
|
|
288
|
+
let waits = 0;
|
|
289
|
+
let inFlight = 0;
|
|
290
|
+
let blockedSince = 0;
|
|
291
|
+
return {
|
|
292
|
+
clock: {
|
|
293
|
+
now: () => base.now(),
|
|
294
|
+
sleep: async (ms) => {
|
|
295
|
+
// A zero-length sleep is a scheduling yield, not a wait — `setCooldown` issues one on every
|
|
296
|
+
// budget rollover, and counting those would report a wait per ten requests that never happened.
|
|
297
|
+
if (ms <= 0)
|
|
298
|
+
return base.sleep(ms);
|
|
299
|
+
if (inFlight === 0) {
|
|
300
|
+
blockedSince = base.now();
|
|
301
|
+
}
|
|
302
|
+
inFlight += 1;
|
|
303
|
+
waits += 1;
|
|
304
|
+
try {
|
|
305
|
+
await base.sleep(ms);
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
inFlight -= 1;
|
|
309
|
+
if (inFlight === 0) {
|
|
310
|
+
waitingMs += Math.max(base.now() - blockedSince, 0);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
read: (cooldowns) => ({
|
|
316
|
+
elapsedMs: base.now() - startedAt,
|
|
317
|
+
waitingMs,
|
|
318
|
+
waits,
|
|
319
|
+
cooldowns,
|
|
320
|
+
}),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Create an FCC Broadband Data Collection public-API client. See the file header for the full rationale.
|
|
325
|
+
*
|
|
326
|
+
* Throws immediately, before any request is made, when constructed without explicit credentials AND without the
|
|
327
|
+
* `FCC_MAP_USERNAME`/`FCC_MAP_API_KEY` environment values.
|
|
328
|
+
*/
|
|
329
|
+
export function createBDCClient(options = {}) {
|
|
330
|
+
const username = options.username ?? $private.FCC_MAP_USERNAME;
|
|
331
|
+
const apiKey = options.apiKey ?? $private.FCC_MAP_API_KEY;
|
|
332
|
+
if (!username || !apiKey) {
|
|
333
|
+
throw new Error("createBDCClient: missing FCC Broadband Map credentials. Pass `username`/`apiKey` explicitly, or set the " +
|
|
334
|
+
"`FCC_MAP_USERNAME`/`FCC_MAP_API_KEY` environment variables (register at https://broadbandmap.fcc.gov " +
|
|
335
|
+
"to obtain them).");
|
|
336
|
+
}
|
|
337
|
+
const requestsPerMinute = Math.max(1, options.requestsPerMinute ?? BDC_DEFAULT_REQUESTS_PER_MINUTE);
|
|
338
|
+
const meter = createMeteredClock(options.clock ?? systemClock);
|
|
339
|
+
let cooldowns = 0;
|
|
340
|
+
const client = new BDCClient({
|
|
341
|
+
displayName: "FCC BDC",
|
|
342
|
+
username,
|
|
343
|
+
downloadTimeoutMs: options.downloadTimeoutMs ?? DEFAULT_DOWNLOAD_TIMEOUT_MS,
|
|
344
|
+
readThrottleStats: () => meter.read(cooldowns),
|
|
345
|
+
// BOTH GATES, on purpose, and the interval is the one that holds the rate.
|
|
346
|
+
//
|
|
347
|
+
// `requestsPerMinute` alone does NOT deliver N requests per minute. It is a budget model whose
|
|
348
|
+
// cooldown is `MS_PER_MINUTE / N` minus the gap since the previous dispatch — so N dispatches go out
|
|
349
|
+
// back to back and the client then waits 60000/N ms, i.e. N requests every 60/N SECONDS. Measured
|
|
350
|
+
// against a bare `APIClient` at `requestsPerMinute: 10` with a 20-call fan-out on a virtual clock:
|
|
351
|
+
// arrivals at `[0 x10, 6000 x10]`, i.e. 20 inside one sliding minute against a budget of 10, and a
|
|
352
|
+
// sustained 100 requests/minute — ten times the published limit. `minRequestIntervalMs` is the gate
|
|
353
|
+
// that actually spaces dispatches, and it is what makes this client honor 10/minute.
|
|
354
|
+
//
|
|
355
|
+
// The budget is still declared rather than dropped — and it is NOT free. The two gates compose (both
|
|
356
|
+
// must clear), so the budget's cooldown still fires, and it is a REAL wait: `APIClient` measures that
|
|
357
|
+
// cooldown to the end of the MINUTE the window opened in, while the interval gate has by then spent
|
|
358
|
+
// only `(N-1) * 60000/N` ms of it. With both gates on 10/minute, arrivals run `0, 6, …, 54 s`; the
|
|
359
|
+
// 10th dispatch opens a `60000 - 54000 = 6000 ms` cooldown; the pacer's grant for #11 is discarded
|
|
360
|
+
// across that wait (`acquireDispatchSlot` re-acquires rather than holding a stale grant, under-issuing
|
|
361
|
+
// by one — the safe direction), so #11 lands at 66 s and the pattern repeats. Steady state is 10
|
|
362
|
+
// requests per 66 s, ~9.1/minute — BELOW the published 10/minute, which is the conservative direction
|
|
363
|
+
// and the reason this composition is left as is.
|
|
364
|
+
//
|
|
365
|
+
// Declaring the budget states the intent in the option whose name matches the published limit. If
|
|
366
|
+
// `requestsPerMinute` is ever corrected in `core/api` to mean what it says, this client already
|
|
367
|
+
// declares the right budget and the interval becomes a redundant second ceiling rather than the
|
|
368
|
+
// load-bearing one.
|
|
369
|
+
requestsPerMinute,
|
|
370
|
+
minRequestIntervalMs: Math.ceil(MS_PER_MINUTE / requestsPerMinute),
|
|
371
|
+
retry: {
|
|
372
|
+
maxAttempts: options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
373
|
+
baseDelayMs: options.baseRetryDelayMs ?? DEFAULT_BASE_RETRY_DELAY_MS,
|
|
374
|
+
},
|
|
375
|
+
clock: meter.clock,
|
|
376
|
+
caching: {
|
|
377
|
+
storage: buildDiskStorage({
|
|
378
|
+
directory: options.cacheDir ?? dataRootPath("bdc", "cache", "http"),
|
|
379
|
+
// Validate BEFORE writing. Every BDC endpoint answers with a `data`-keyed envelope, so a
|
|
380
|
+
// decoded body that isn't one means the upstream served something other than what it
|
|
381
|
+
// claimed — an error page, a login redirect — and persisting that would hand the next run a
|
|
382
|
+
// body its caller will destructure into `undefined`.
|
|
383
|
+
validate: (value) => isBDCEnvelope(value.data?.data),
|
|
384
|
+
}),
|
|
385
|
+
ttl: options.cacheTTLMs ?? DEFAULT_CACHE_TTL_MS,
|
|
386
|
+
// The TTL above is chosen against the FCC's filing cadence. Letting a response header override
|
|
387
|
+
// it would silently replace that reasoning with whatever the CDN in front of broadbandmap.fcc.gov
|
|
388
|
+
// happens to send.
|
|
389
|
+
interpretHeader: false,
|
|
390
|
+
// NO `cachePredicate` HERE, deliberately. "Never cache a failure" is a real property and it is
|
|
391
|
+
// tested, but it is already guaranteed upstream: Axios's default `validateStatus` rejects
|
|
392
|
+
// anything outside 200-299 before the cache interceptor's response hook ever runs, so the
|
|
393
|
+
// predicate only ever sees a 2xx and a narrower `statusCheck` cannot change any outcome. A
|
|
394
|
+
// narrowed one was written here first and MUTATION-PROVED UNFALSIFIABLE — deleting it caused zero
|
|
395
|
+
// test failures — so it was removed rather than left as decoration. (It could not have been
|
|
396
|
+
// harmful either: the interceptor consults the predicate only when there is no existing entry,
|
|
397
|
+
// `!cache.data && !testCachePredicate(...)`, so it never sees a 304 revalidation.)
|
|
398
|
+
},
|
|
399
|
+
axios: {
|
|
400
|
+
headers: {
|
|
401
|
+
// NOT bearer, NOT basic — the BDC API takes the credential pair as two plain headers.
|
|
402
|
+
username,
|
|
403
|
+
hash_value: apiKey,
|
|
404
|
+
},
|
|
405
|
+
timeout: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
406
|
+
responseType: "json",
|
|
407
|
+
// `silentJSONParsing` defaults to TRUE, which makes Axios hand back the RAW STRING when a body
|
|
408
|
+
// fails to parse instead of raising. An upstream serving an HTML error page under a 200 would
|
|
409
|
+
// then be returned as `T` and destructured into `undefined` at the call site; parse failures
|
|
410
|
+
// must be errors.
|
|
411
|
+
transitional: { silentJSONParsing: false },
|
|
412
|
+
...options.axios,
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
client.addEventListener("cooldown_start", () => {
|
|
416
|
+
cooldowns++;
|
|
417
|
+
client.logger.debug("Per-minute request budget spent — waiting for the cooldown to lapse.");
|
|
418
|
+
});
|
|
419
|
+
client.addEventListener("cooldown_end", () => {
|
|
420
|
+
client.logger.debug("Request budget cooldown lapsed.");
|
|
421
|
+
});
|
|
422
|
+
return client;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Whether a decoded response body is the `{ data: … }` envelope every BDC endpoint answers with.
|
|
426
|
+
*/
|
|
427
|
+
function isBDCEnvelope(body) {
|
|
428
|
+
return typeof body === "object" && body !== null && "data" in body;
|
|
429
|
+
}
|
|
430
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../sdk/client.ts"],"names":[],"mappings":"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"}
|