@seta-labs/geo 0.7.0 → 0.9.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/CHANGELOG.md +25 -0
- package/README.md +30 -9
- package/dist/index.d.ts +110 -14
- package/dist/index.js +129 -13
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @seta-labs/geo
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
- `territory.raggi(input)`: for each point and municipality (`codComune`), the
|
|
6
|
+
radius that covers the whole municipality from the point: the distance to
|
|
7
|
+
the farthest vertex of its ISTAT boundary, in metres rounded up, from the
|
|
8
|
+
latest edition that has it (a suppressed municipality keeps its last).
|
|
9
|
+
`edizione` and `raggioM` are null when no edition has the municipality. Up
|
|
10
|
+
to 100 points per call; `id` is echoed back.
|
|
11
|
+
|
|
12
|
+
## 0.8.0
|
|
13
|
+
|
|
14
|
+
- **Breaking:** `LookupCache` becomes `CachePolicy`, the cache options every
|
|
15
|
+
Seta service and SDK shares: `refresh` is now `noCache`, like the header
|
|
16
|
+
directive; `noStore`, `maxAge` and `onlyIfCached` keep their names. The type
|
|
17
|
+
and its header writer come from `@seta-labs/nestjs-cache-control/policy`,
|
|
18
|
+
bundled into the SDK: nothing more to install.
|
|
19
|
+
- Each client paces its calls (`pacing`): at most 10 in flight, a `429` or
|
|
20
|
+
`503` sent again up to 4 times after its `Retry-After` (up to a minute;
|
|
21
|
+
1-8 s backoff without it), the whole queue waiting meanwhile. `pacing:
|
|
22
|
+
{ concurrency, retries }` tunes it, `pacing: false` turns it off; two queues
|
|
23
|
+
are two clients.
|
|
24
|
+
- `timeoutMs` (default 30 s) bounds each attempt, not the wait in the queue.
|
|
25
|
+
- `ApiError.code`: Geo's error code (`CADASTRE_BLOCKED`, `NOT_CACHED`, ...)
|
|
26
|
+
read from the body.
|
|
27
|
+
|
|
3
28
|
## 0.7.0
|
|
4
29
|
|
|
5
30
|
- `reverseGeocode(input)` answers from a chain of sources: the ANNCSU register
|
package/README.md
CHANGED
|
@@ -40,9 +40,20 @@ Do not use `$fetch` as `fetch`: it returns the parsed body instead of a Response
|
|
|
40
40
|
|
|
41
41
|
All calls return `Promise<T>` and accept optional `{ signal }` as the last
|
|
42
42
|
argument. An optional `fetch: (request: Request) => Promise<Response>` allows
|
|
43
|
-
consumer-owned instrumentation or transport adapters.
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
consumer-owned instrumentation or transport adapters.
|
|
44
|
+
|
|
45
|
+
Each client paces its calls: at most 10 in flight, and a `429` or `503` that
|
|
46
|
+
says when to come back (`Retry-After` up to a minute; 1 s, 2 s, 4 s, 8 s
|
|
47
|
+
without it) is sent again up to 4 times while the client's whole queue waits.
|
|
48
|
+
A refusal asking for a longer wait, every other status and network errors
|
|
49
|
+
reach the caller after one attempt. Each attempt has `timeoutMs` (default
|
|
50
|
+
30 s), not counting the wait in the queue. Two queues are two clients:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const geo = createGeoClient({ baseUrl, apiKey }); // lookups, territory
|
|
54
|
+
const cadastre = createGeoClient({ baseUrl, apiKey, pacing: { concurrency: 4 } }); // the map, apart
|
|
55
|
+
const panel = createGeoClient({ baseUrl: '/api', pacing: false }); // every call at once, once
|
|
56
|
+
```
|
|
46
57
|
|
|
47
58
|
| Method | Input |
|
|
48
59
|
| ---------------------------------------- | -------------------------------------- |
|
|
@@ -66,14 +77,24 @@ validation of received data. `include` may be omitted and defaults to `best`
|
|
|
66
77
|
on the service.
|
|
67
78
|
|
|
68
79
|
The four lookups (`geocode`, `geocodeBatch`, `reverseGeocode`,
|
|
69
|
-
`cadastreParcel`) take `cache` in their options
|
|
70
|
-
|
|
71
|
-
`{
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
`cadastreParcel`) take `cache` in their options: a `CachePolicy`, the shape
|
|
81
|
+
every Seta service and SDK shares (`@seta-labs/nestjs-cache-control/policy`,
|
|
82
|
+
bundled here), sent as `Cache-Control`. `{ noCache: true }` asks the provider
|
|
83
|
+
again and stores the new answer, `{ noCache: true, noStore: true }` bypasses
|
|
84
|
+
the store entirely, `{ maxAge }` asks again past that many seconds,
|
|
85
|
+
`{ onlyIfCached: true }` never asks the provider (504 `NOT_CACHED`, or
|
|
86
|
+
`status: 'NOT_CACHED'` for a batch item). Without `cache` the store answers
|
|
87
|
+
when it can and the provider otherwise. The other calls read Geo's own data
|
|
88
|
+
and take only `signal`.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
await geo.reverseGeocode({ lat, lng }, { cache: { onlyIfCached: true } });
|
|
92
|
+
await geo.reverseGeocode({ lat, lng }, { cache: jobCachePolicy() }); // the job's, at each call
|
|
93
|
+
```
|
|
74
94
|
|
|
75
95
|
HTTP errors are `ApiError`; their body is JSON when parseable, otherwise text,
|
|
76
|
-
or `undefined` for an empty response
|
|
96
|
+
or `undefined` for an empty response, and `code` is Geo's error code when the
|
|
97
|
+
body has one. A `503` from `cadastreParcel` with
|
|
77
98
|
`code: 'CADASTRE_BLOCKED'` means the cadastral map is refusing calls: its
|
|
78
99
|
`Retry-After` header (and `retryAfterSeconds` in the body) says when to ask
|
|
79
100
|
again. Network failures, aborts and JSON parsing
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ declare class ApiError extends Error {
|
|
|
4
4
|
readonly status: number;
|
|
5
5
|
readonly headers: Headers;
|
|
6
6
|
readonly requestId: string | undefined;
|
|
7
|
+
/** Geo's error `code` (`CADASTRE_BLOCKED`, `NOT_CACHED`, ...), when the body has one. */
|
|
8
|
+
readonly code: string | undefined;
|
|
7
9
|
constructor(response: Response, body: unknown);
|
|
8
10
|
}
|
|
9
11
|
|
|
@@ -223,6 +225,22 @@ interface paths {
|
|
|
223
225
|
patch?: never;
|
|
224
226
|
trace?: never;
|
|
225
227
|
};
|
|
228
|
+
'/territory/raggi': {
|
|
229
|
+
parameters: {
|
|
230
|
+
query?: never;
|
|
231
|
+
header?: never;
|
|
232
|
+
path?: never;
|
|
233
|
+
cookie?: never;
|
|
234
|
+
};
|
|
235
|
+
get?: never;
|
|
236
|
+
put?: never;
|
|
237
|
+
post: operations['TerritoryController_raggi'];
|
|
238
|
+
delete?: never;
|
|
239
|
+
options?: never;
|
|
240
|
+
head?: never;
|
|
241
|
+
patch?: never;
|
|
242
|
+
trace?: never;
|
|
243
|
+
};
|
|
226
244
|
'/cadastre/parcel': {
|
|
227
245
|
parameters: {
|
|
228
246
|
query?: never;
|
|
@@ -672,6 +690,30 @@ interface components {
|
|
|
672
690
|
TerritoryResolveResponseDto: {
|
|
673
691
|
items: components['schemas']['TerritoryPointResultDto'][];
|
|
674
692
|
};
|
|
693
|
+
TerritoryRaggioPointDto: {
|
|
694
|
+
id: string;
|
|
695
|
+
/** @description Belfiore code of the municipality (e.g. `D969`). */
|
|
696
|
+
codComune: string;
|
|
697
|
+
lat: number;
|
|
698
|
+
lng: number;
|
|
699
|
+
};
|
|
700
|
+
TerritoryRaggiRequestDto: {
|
|
701
|
+
points: components['schemas']['TerritoryRaggioPointDto'][];
|
|
702
|
+
};
|
|
703
|
+
TerritoryRaggioDto: {
|
|
704
|
+
id: string;
|
|
705
|
+
codComune: string;
|
|
706
|
+
/**
|
|
707
|
+
* Format: date
|
|
708
|
+
* @description Boundary edition.
|
|
709
|
+
*/
|
|
710
|
+
edizione: string | null;
|
|
711
|
+
/** @description Metres, rounded up. */
|
|
712
|
+
raggioM: number | null;
|
|
713
|
+
};
|
|
714
|
+
TerritoryRaggiResponseDto: {
|
|
715
|
+
items: components['schemas']['TerritoryRaggioDto'][];
|
|
716
|
+
};
|
|
675
717
|
CadastreParcelRequestDto: {
|
|
676
718
|
lat: number;
|
|
677
719
|
lng: number;
|
|
@@ -1171,6 +1213,36 @@ interface operations {
|
|
|
1171
1213
|
};
|
|
1172
1214
|
};
|
|
1173
1215
|
};
|
|
1216
|
+
TerritoryController_raggi: {
|
|
1217
|
+
parameters: {
|
|
1218
|
+
query?: never;
|
|
1219
|
+
header?: never;
|
|
1220
|
+
path?: never;
|
|
1221
|
+
cookie?: never;
|
|
1222
|
+
};
|
|
1223
|
+
requestBody: {
|
|
1224
|
+
content: {
|
|
1225
|
+
'application/json': components['schemas']['TerritoryRaggiRequestDto'];
|
|
1226
|
+
};
|
|
1227
|
+
};
|
|
1228
|
+
responses: {
|
|
1229
|
+
200: {
|
|
1230
|
+
headers: {
|
|
1231
|
+
[name: string]: unknown;
|
|
1232
|
+
};
|
|
1233
|
+
content: {
|
|
1234
|
+
'application/json': components['schemas']['TerritoryRaggiResponseDto'];
|
|
1235
|
+
};
|
|
1236
|
+
};
|
|
1237
|
+
/** @description Missing or invalid API key. */
|
|
1238
|
+
401: {
|
|
1239
|
+
headers: {
|
|
1240
|
+
[name: string]: unknown;
|
|
1241
|
+
};
|
|
1242
|
+
content?: never;
|
|
1243
|
+
};
|
|
1244
|
+
};
|
|
1245
|
+
};
|
|
1174
1246
|
CadastreController_parcel: {
|
|
1175
1247
|
parameters: {
|
|
1176
1248
|
query?: never;
|
|
@@ -1282,6 +1354,9 @@ type TerritoryStatus = ApiSchemas['TerritoryStatusDto'];
|
|
|
1282
1354
|
type TerritoryResolveRequest = ApiSchemas['TerritoryResolveRequestDto'];
|
|
1283
1355
|
type TerritoryResolveResponse = ApiSchemas['TerritoryResolveResponseDto'];
|
|
1284
1356
|
type TerritoryPointResult = ApiSchemas['TerritoryPointResultDto'];
|
|
1357
|
+
type TerritoryRaggiRequest = ApiSchemas['TerritoryRaggiRequestDto'];
|
|
1358
|
+
type TerritoryRaggiResponse = ApiSchemas['TerritoryRaggiResponseDto'];
|
|
1359
|
+
type TerritoryRaggio = ApiSchemas['TerritoryRaggioDto'];
|
|
1285
1360
|
type TerritoryOmi = ApiSchemas['TerritoryOmiDto'];
|
|
1286
1361
|
type TerritoryComune = ApiSchemas['TerritoryComuneDto'];
|
|
1287
1362
|
type TerritoryComuneDetail = ApiSchemas['TerritoryComuneDetailDto'];
|
|
@@ -1294,6 +1369,31 @@ type TerritoryQuotazione = ApiSchemas['TerritoryQuotazioneDto'];
|
|
|
1294
1369
|
type TerritoryCompravenditePage = ApiSchemas['TerritoryCompravenditePageDto'];
|
|
1295
1370
|
type TerritoryCompravendita = ApiSchemas['TerritoryCompravenditaDto'];
|
|
1296
1371
|
|
|
1372
|
+
/**
|
|
1373
|
+
* The request directives of RFC 9111 a lookup honours; any other directive is
|
|
1374
|
+
* ignored. One shape for both sides: the server reads it from the header
|
|
1375
|
+
* (`parseCacheControl`, every flag set), a client writes it
|
|
1376
|
+
* (`formatCacheControl`, an absent flag is false).
|
|
1377
|
+
*/
|
|
1378
|
+
interface CachePolicy {
|
|
1379
|
+
/** `no-cache`: never answer from the store without asking the provider. */
|
|
1380
|
+
noCache?: boolean;
|
|
1381
|
+
/** `no-store`: never write the provider's answer to the store. */
|
|
1382
|
+
noStore?: boolean;
|
|
1383
|
+
/** `only-if-cached`: never ask the provider; without a usable row, refuse. */
|
|
1384
|
+
onlyIfCached?: boolean;
|
|
1385
|
+
/** `max-age=N`: a stored row older than N seconds is not usable. */
|
|
1386
|
+
maxAge?: number;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/** How a client paces its calls to Geo; each client has its own queue. */
|
|
1390
|
+
interface PacingOptions {
|
|
1391
|
+
/** Requests in flight at once; default 10. */
|
|
1392
|
+
concurrency?: number;
|
|
1393
|
+
/** New attempts after a 429 or 503 that says when to come back; default 4 (5 attempts in all). */
|
|
1394
|
+
retries?: number;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1297
1397
|
type TerritoryComuniQuery = NonNullable<ApiPaths['/territory/comuni']['get']['parameters']['query']>;
|
|
1298
1398
|
type TerritoryQuotazioniQuery = NonNullable<ApiPaths['/territory/quotazioni']['get']['parameters']['query']>;
|
|
1299
1399
|
type TerritoryCompravenditeQuery = NonNullable<ApiPaths['/territory/compravendite']['get']['parameters']['query']>;
|
|
@@ -1302,21 +1402,12 @@ interface RequestOptions {
|
|
|
1302
1402
|
signal?: AbortSignal;
|
|
1303
1403
|
}
|
|
1304
1404
|
/**
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1405
|
+
* The options of a lookup, the four calls Geo answers from its store:
|
|
1406
|
+
* `cache` is sent as `Cache-Control` (RFC 9111). Without it Geo answers
|
|
1407
|
+
* from the store when it can and asks the provider otherwise.
|
|
1307
1408
|
*/
|
|
1308
|
-
interface LookupCache {
|
|
1309
|
-
/** `no-cache`: ask the provider even with a stored answer, and store the new one. */
|
|
1310
|
-
refresh?: boolean;
|
|
1311
|
-
/** `no-store`: never write the provider's answer (with `refresh`: a pure bypass). */
|
|
1312
|
-
noStore?: boolean;
|
|
1313
|
-
/** `max-age`: a stored answer older than this many seconds is asked again. */
|
|
1314
|
-
maxAge?: number;
|
|
1315
|
-
/** `only-if-cached`: never ask the provider; 504 `NOT_CACHED` without a stored answer. */
|
|
1316
|
-
onlyIfCached?: boolean;
|
|
1317
|
-
}
|
|
1318
1409
|
interface LookupOptions extends RequestOptions {
|
|
1319
|
-
cache?:
|
|
1410
|
+
cache?: CachePolicy;
|
|
1320
1411
|
}
|
|
1321
1412
|
interface GeoClientOptions {
|
|
1322
1413
|
/** Complete API root, including /api. Relative roots are supported in browsers. */
|
|
@@ -1325,6 +1416,10 @@ interface GeoClientOptions {
|
|
|
1325
1416
|
apiKey?: string;
|
|
1326
1417
|
/** A real Fetch implementation returning a Response, not a parsed-body $fetch. */
|
|
1327
1418
|
fetch?: (request: Request) => Promise<Response>;
|
|
1419
|
+
/** Each attempt's deadline, not counting the wait in the queue; default 30 s. */
|
|
1420
|
+
timeoutMs?: number;
|
|
1421
|
+
/** The client's queue and its retries on 429/503; `false` sends every call at once, once. */
|
|
1422
|
+
pacing?: false | PacingOptions;
|
|
1328
1423
|
}
|
|
1329
1424
|
interface TerritoryClient {
|
|
1330
1425
|
status(options?: RequestOptions): Promise<TerritoryStatus>;
|
|
@@ -1334,6 +1429,7 @@ interface TerritoryClient {
|
|
|
1334
1429
|
compravendite(query?: TerritoryCompravenditeQuery, options?: RequestOptions): Promise<TerritoryCompravenditePage>;
|
|
1335
1430
|
comune(codComune: string, options?: RequestOptions): Promise<TerritoryComuneDetail>;
|
|
1336
1431
|
resolve(input: TerritoryResolveRequest, options?: RequestOptions): Promise<TerritoryResolveResponse>;
|
|
1432
|
+
raggi(input: TerritoryRaggiRequest, options?: RequestOptions): Promise<TerritoryRaggiResponse>;
|
|
1337
1433
|
}
|
|
1338
1434
|
interface GeoClient {
|
|
1339
1435
|
geocode(input: GeocodeRequest, options?: LookupOptions): Promise<GeocodeResult>;
|
|
@@ -1350,4 +1446,4 @@ interface GeoClient {
|
|
|
1350
1446
|
declare function createGeoClient(options: GeoClientOptions): GeoClient;
|
|
1351
1447
|
|
|
1352
1448
|
export { ApiError, createGeoClient };
|
|
1353
|
-
export type { CadastreParcelRequest, CadastreParcelResult, CadastreParcelRow, CadastreParcelsPage, CadastreParcelsQuery, GeoAdminArea, GeoClient, GeoClientOptions, GeoStreetMatch, GeocodeBatchRequest, GeocodeBatchResponse, GeocodeRequest, GeocodeResult,
|
|
1449
|
+
export type { CachePolicy, CadastreParcelRequest, CadastreParcelResult, CadastreParcelRow, CadastreParcelsPage, CadastreParcelsQuery, GeoAdminArea, GeoClient, GeoClientOptions, GeoStreetMatch, GeocodeBatchRequest, GeocodeBatchResponse, GeocodeRequest, GeocodeResult, LookupOptions, PacingOptions, RequestOptions, ReverseGeocodeRequest, ReverseGeocodeResult, TerritoryClient, TerritoryCompravendita, TerritoryCompravenditePage, TerritoryCompravenditeQuery, TerritoryComune, TerritoryComuneDetail, TerritoryComuneRiga, TerritoryComuniPage, TerritoryComuniQuery, TerritoryDatasets, TerritoryOmi, TerritoryPointResult, TerritoryQuotazione, TerritoryQuotazioniPage, TerritoryQuotazioniQuery, TerritoryRaggiRequest, TerritoryRaggiResponse, TerritoryRaggio, TerritoryResolveRequest, TerritoryResolveResponse, TerritoryStatus, TerritoryTerritorio };
|
package/dist/index.js
CHANGED
|
@@ -7,11 +7,14 @@ var ApiError = class extends Error {
|
|
|
7
7
|
this.status = response.status;
|
|
8
8
|
this.headers = new Headers(response.headers);
|
|
9
9
|
this.requestId = response.headers.get("x-request-id") ?? void 0;
|
|
10
|
+
this.code = codeOf(body);
|
|
10
11
|
}
|
|
11
12
|
body;
|
|
12
13
|
status;
|
|
13
14
|
headers;
|
|
14
15
|
requestId;
|
|
16
|
+
/** Geo's error `code` (`CADASTRE_BLOCKED`, `NOT_CACHED`, ...), when the body has one. */
|
|
17
|
+
code;
|
|
15
18
|
};
|
|
16
19
|
async function readErrorBody(response) {
|
|
17
20
|
const text = await response.text();
|
|
@@ -22,9 +25,126 @@ async function readErrorBody(response) {
|
|
|
22
25
|
return text;
|
|
23
26
|
}
|
|
24
27
|
}
|
|
28
|
+
function codeOf(body) {
|
|
29
|
+
if (typeof body !== "object" || body === null || !("code" in body)) return void 0;
|
|
30
|
+
return typeof body.code === "string" ? body.code : void 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ../../node_modules/.pnpm/@seta-labs+nestjs-cache-control@0.2.1_@nestjs+common@12.3.2_class-transformer@0.5.1_cla_e752cf18c930d1bfb41f37f56b7e15c5/node_modules/@seta-labs/nestjs-cache-control/dist/cache-policy.util.js
|
|
34
|
+
var MAX_DELTA_SECONDS = 2147483648;
|
|
35
|
+
function formatCacheControl(policy) {
|
|
36
|
+
if (!policy)
|
|
37
|
+
return void 0;
|
|
38
|
+
const maxAge = policy.maxAge;
|
|
39
|
+
const directives = [
|
|
40
|
+
policy.noCache ? "no-cache" : void 0,
|
|
41
|
+
policy.noStore ? "no-store" : void 0,
|
|
42
|
+
maxAge !== void 0 && Number.isFinite(maxAge) ? `max-age=${Math.min(Math.max(0, Math.floor(maxAge)), MAX_DELTA_SECONDS)}` : void 0,
|
|
43
|
+
policy.onlyIfCached ? "only-if-cached" : void 0
|
|
44
|
+
].filter((directive) => directive !== void 0);
|
|
45
|
+
return directives.length > 0 ? directives.join(", ") : void 0;
|
|
46
|
+
}
|
|
25
47
|
|
|
26
48
|
// src/client.ts
|
|
27
49
|
import createClient from "openapi-fetch";
|
|
50
|
+
|
|
51
|
+
// src/pacing.ts
|
|
52
|
+
var DEFAULT_CONCURRENCY = 10;
|
|
53
|
+
var DEFAULT_RETRIES = 4;
|
|
54
|
+
var RETRY_STATUSES = /* @__PURE__ */ new Set([429, 503]);
|
|
55
|
+
var BACKOFF_MS = 1e3;
|
|
56
|
+
var MAX_WAIT_MS = 6e4;
|
|
57
|
+
function pacedFetch(fetch, options = {}) {
|
|
58
|
+
const run = limiter(options.concurrency ?? DEFAULT_CONCURRENCY);
|
|
59
|
+
const retries = options.retries ?? DEFAULT_RETRIES;
|
|
60
|
+
let pausedUntil = 0;
|
|
61
|
+
const send = async (request, attempt) => {
|
|
62
|
+
while (pausedUntil > Date.now()) await sleepUntil(pausedUntil, request.signal);
|
|
63
|
+
const response = await fetch(request.clone());
|
|
64
|
+
const waitMs = attempt < retries ? retryDelay(response, attempt) : void 0;
|
|
65
|
+
if (waitMs !== void 0) pausedUntil = Math.max(pausedUntil, Date.now() + waitMs);
|
|
66
|
+
return { response, waitMs };
|
|
67
|
+
};
|
|
68
|
+
return async (request) => {
|
|
69
|
+
for (let attempt = 0; ; attempt++) {
|
|
70
|
+
const { response, waitMs } = await run(request.signal, () => send(request, attempt));
|
|
71
|
+
if (waitMs === void 0) return response;
|
|
72
|
+
await response.body?.cancel();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function timedFetch(fetch, timeoutMs) {
|
|
77
|
+
return (request) => fetch(
|
|
78
|
+
new Request(request, {
|
|
79
|
+
signal: AbortSignal.any([request.signal, AbortSignal.timeout(timeoutMs)])
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
function retryDelay(response, attempt) {
|
|
84
|
+
if (!RETRY_STATUSES.has(response.status)) return void 0;
|
|
85
|
+
const header = response.headers.get("retry-after");
|
|
86
|
+
const waitMs = header === null ? BACKOFF_MS * 2 ** attempt : retryAfterMs(header);
|
|
87
|
+
return waitMs !== void 0 && waitMs <= MAX_WAIT_MS ? waitMs : void 0;
|
|
88
|
+
}
|
|
89
|
+
function retryAfterMs(header) {
|
|
90
|
+
const value = header.trim();
|
|
91
|
+
if (/^\d+$/.test(value)) return Number(value) * 1e3;
|
|
92
|
+
const date = Date.parse(value);
|
|
93
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
|
|
94
|
+
}
|
|
95
|
+
function limiter(concurrency) {
|
|
96
|
+
let active = 0;
|
|
97
|
+
const waiting = [];
|
|
98
|
+
const acquire = (signal) => {
|
|
99
|
+
signal.throwIfAborted();
|
|
100
|
+
if (active < concurrency) {
|
|
101
|
+
active++;
|
|
102
|
+
return Promise.resolve();
|
|
103
|
+
}
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const start = () => {
|
|
106
|
+
signal.removeEventListener("abort", abort);
|
|
107
|
+
active++;
|
|
108
|
+
resolve();
|
|
109
|
+
};
|
|
110
|
+
const abort = () => {
|
|
111
|
+
waiting.splice(waiting.indexOf(start), 1);
|
|
112
|
+
reject(signal.reason);
|
|
113
|
+
};
|
|
114
|
+
waiting.push(start);
|
|
115
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
return async (signal, task) => {
|
|
119
|
+
await acquire(signal);
|
|
120
|
+
try {
|
|
121
|
+
return await task();
|
|
122
|
+
} finally {
|
|
123
|
+
active--;
|
|
124
|
+
waiting.shift()?.();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function sleepUntil(time, signal) {
|
|
129
|
+
const delay = time - Date.now();
|
|
130
|
+
if (delay <= 0) return Promise.resolve();
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
signal.throwIfAborted();
|
|
133
|
+
const timer = setTimeout(done, delay);
|
|
134
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
135
|
+
function done() {
|
|
136
|
+
signal.removeEventListener("abort", abort);
|
|
137
|
+
resolve();
|
|
138
|
+
}
|
|
139
|
+
function abort() {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
reject(signal.reason);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/client.ts
|
|
147
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
28
148
|
function apiBaseUrl(baseUrl) {
|
|
29
149
|
const url = new URL(baseUrl, globalThis.location?.href);
|
|
30
150
|
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
@@ -39,19 +159,9 @@ async function data(request) {
|
|
|
39
159
|
if (result.data === void 0) throw new SyntaxError("Geo API returned an empty JSON response");
|
|
40
160
|
return result.data;
|
|
41
161
|
}
|
|
42
|
-
function cacheControl(cache) {
|
|
43
|
-
if (!cache) return void 0;
|
|
44
|
-
const directives = [
|
|
45
|
-
cache.refresh ? "no-cache" : void 0,
|
|
46
|
-
cache.noStore ? "no-store" : void 0,
|
|
47
|
-
cache.maxAge === void 0 ? void 0 : `max-age=${Math.max(0, Math.floor(cache.maxAge))}`,
|
|
48
|
-
cache.onlyIfCached ? "only-if-cached" : void 0
|
|
49
|
-
].filter((directive) => directive !== void 0);
|
|
50
|
-
return directives.length > 0 ? directives.join(", ") : void 0;
|
|
51
|
-
}
|
|
52
162
|
function lookupInit(options) {
|
|
53
163
|
const { cache, ...request } = options ?? {};
|
|
54
|
-
const header =
|
|
164
|
+
const header = formatCacheControl(cache);
|
|
55
165
|
return header ? { ...request, headers: { "cache-control": header } } : request;
|
|
56
166
|
}
|
|
57
167
|
function territory(client) {
|
|
@@ -67,13 +177,14 @@ function territory(client) {
|
|
|
67
177
|
params: { path: { codComune } }
|
|
68
178
|
})
|
|
69
179
|
),
|
|
70
|
-
resolve: (input, options) => data(client.POST("/territory/resolve", { ...options, body: input }))
|
|
180
|
+
resolve: (input, options) => data(client.POST("/territory/resolve", { ...options, body: input })),
|
|
181
|
+
raggi: (input, options) => data(client.POST("/territory/raggi", { ...options, body: input }))
|
|
71
182
|
};
|
|
72
183
|
}
|
|
73
184
|
function createGeoClient(options) {
|
|
74
185
|
const client = createClient({
|
|
75
186
|
baseUrl: apiBaseUrl(options.baseUrl),
|
|
76
|
-
fetch: options
|
|
187
|
+
fetch: transport(options),
|
|
77
188
|
headers: options.apiKey ? { "x-api-key": options.apiKey } : void 0
|
|
78
189
|
});
|
|
79
190
|
client.use({
|
|
@@ -90,6 +201,11 @@ function createGeoClient(options) {
|
|
|
90
201
|
territory: territory(client)
|
|
91
202
|
};
|
|
92
203
|
}
|
|
204
|
+
function transport(options) {
|
|
205
|
+
const send = options.fetch ?? ((request) => globalThis.fetch(request));
|
|
206
|
+
const timed = timedFetch(send, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
207
|
+
return options.pacing === false ? timed : pacedFetch(timed, options.pacing);
|
|
208
|
+
}
|
|
93
209
|
export {
|
|
94
210
|
ApiError,
|
|
95
211
|
createGeoClient
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seta-labs/geo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Typed Fetch client for the Seta Geo HTTP API",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -43,9 +43,10 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@seta-geo/types": "workspace:*",
|
|
46
|
-
"@seta-labs/config": "^0.16.
|
|
46
|
+
"@seta-labs/config": "^0.16.2",
|
|
47
|
+
"@seta-labs/nestjs-cache-control": "0.2.1",
|
|
47
48
|
"tsup": "^9.0.0",
|
|
48
49
|
"typescript": "^6.0.3",
|
|
49
|
-
"vitest": "^5.0.
|
|
50
|
+
"vitest": "^5.0.5"
|
|
50
51
|
}
|
|
51
52
|
}
|