@nshiab/simple-data-analysis-core 2.0.2 → 2.0.4

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.
Files changed (43) hide show
  1. package/esm/class/SimpleTable.d.ts +50 -32
  2. package/esm/class/SimpleTable.d.ts.map +1 -1
  3. package/esm/class/SimpleTable.js +46 -32
  4. package/esm/helpers/loadOsmFile.d.ts +1 -3
  5. package/esm/helpers/loadOsmFile.d.ts.map +1 -1
  6. package/esm/helpers/loadOsmFile.js +3 -11
  7. package/esm/helpers/osmCache.d.ts +10 -13
  8. package/esm/helpers/osmCache.d.ts.map +1 -1
  9. package/esm/helpers/osmCache.js +177 -32
  10. package/esm/helpers/osmFiles.d.ts +9 -0
  11. package/esm/helpers/osmFiles.d.ts.map +1 -1
  12. package/esm/helpers/osmFiles.js +234 -23
  13. package/esm/methods/loadGeoData.d.ts.map +1 -1
  14. package/esm/methods/loadGeoData.js +4 -39
  15. package/esm/methods/loadOpenStreetMap.d.ts +24 -0
  16. package/esm/methods/loadOpenStreetMap.d.ts.map +1 -0
  17. package/esm/methods/loadOpenStreetMap.js +368 -0
  18. package/llm.md +79 -46
  19. package/llms.txt +3 -0
  20. package/package.json +1 -1
  21. package/script/class/SimpleTable.d.ts +50 -32
  22. package/script/class/SimpleTable.d.ts.map +1 -1
  23. package/script/class/SimpleTable.js +46 -32
  24. package/script/helpers/loadOsmFile.d.ts +1 -3
  25. package/script/helpers/loadOsmFile.d.ts.map +1 -1
  26. package/script/helpers/loadOsmFile.js +3 -11
  27. package/script/helpers/osmCache.d.ts +10 -13
  28. package/script/helpers/osmCache.d.ts.map +1 -1
  29. package/script/helpers/osmCache.js +177 -65
  30. package/script/helpers/osmFiles.d.ts +9 -0
  31. package/script/helpers/osmFiles.d.ts.map +1 -1
  32. package/script/helpers/osmFiles.js +237 -23
  33. package/script/methods/loadGeoData.d.ts.map +1 -1
  34. package/script/methods/loadGeoData.js +3 -38
  35. package/script/methods/loadOpenStreetMap.d.ts +24 -0
  36. package/script/methods/loadOpenStreetMap.d.ts.map +1 -0
  37. package/script/methods/loadOpenStreetMap.js +375 -0
  38. package/esm/methods/loadOSM.d.ts +0 -17
  39. package/esm/methods/loadOSM.d.ts.map +0 -1
  40. package/esm/methods/loadOSM.js +0 -194
  41. package/script/methods/loadOSM.d.ts +0 -17
  42. package/script/methods/loadOSM.d.ts.map +0 -1
  43. package/script/methods/loadOSM.js +0 -201
@@ -1,41 +1,186 @@
1
- import fileCache, { createFileCacheId } from "./fileCache.js";
1
+ import crypto from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
+ import { cacheEntryExpired } from "./cacheDiagnostics.js";
4
+ import { createFileCacheId } from "./fileCache.js";
5
+ import { formatBytes } from "./osmFiles.js";
2
6
  import mergeOptions from "./mergeOptions.js";
3
7
  import parseValue from "./parseValue.js";
8
+ import prettyDuration from "./prettyDuration.js";
4
9
  import queryDB from "./queryDB.js";
5
10
  import quoteIdentifier from "./quoteIdentifier.js";
6
11
  const CACHE_DIRECTORY = ".sda-cache/osm";
7
- const OSM_CACHE_FORMAT_VERSION = "osm-geoparquet-v1";
12
+ const OSM_CACHE_FORMAT_VERSION = "osm-geoparquet-v2";
8
13
  export function createOsmCacheId(identity) {
9
- return createFileCacheId("osm", identity);
14
+ return createFileCacheId("osm", [OSM_CACHE_FORMAT_VERSION, identity]);
10
15
  }
11
- export async function useProcessedOsmCache(table, id, enabled, parameters, compute) {
12
- await fileCache(table, {
13
- directory: CACHE_DIRECTORY,
14
- enabled,
15
- extension: "geoparquet",
16
+ export async function useProcessedOsmCache(table, options) {
17
+ if (!options.enabled) {
18
+ options.verbose && console.log("Processed cache disabled. Fresh data will be used without reading or writing cache entries.");
19
+ await options.compute();
20
+ return;
21
+ }
22
+ const sourcesFile = `${CACHE_DIRECTORY}/sources.json`;
23
+ const sources = readSources(sourcesFile);
24
+ const cached = sources[options.id];
25
+ if (cached !== undefined) {
26
+ if (!isOsmCacheSource(cached)) {
27
+ delete sources[options.id];
28
+ writeSources(sourcesFile, sources);
29
+ console.log("The matching processed cache entry was incomplete and was removed. Fresh data is being requested.");
30
+ }
31
+ else {
32
+ const now = Date.now();
33
+ if (options.ttl !== undefined &&
34
+ cacheEntryExpired(cached.creation, options.ttl, now)) {
35
+ const age = now - cached.creation;
36
+ removeEntry(options.id, cached, sources, sourcesFile);
37
+ console.log(`Processed cache entry expired after ${prettyDuration(0, { end: age })}; ` +
38
+ `the configured TTL is ${prettyDuration(0, { end: options.ttl * 1000 })}.\n` +
39
+ "The matching cache file was removed. Fresh data is being requested.");
40
+ }
41
+ else if (await loadEntry(table, cached, sources, sourcesFile, options)) {
42
+ return;
43
+ }
44
+ }
45
+ }
46
+ else {
47
+ options.verbose &&
48
+ console.log("No processed cache entry matched this source.");
49
+ }
50
+ const started = Date.now();
51
+ await options.compute();
52
+ const computeDuration = Date.now() - started;
53
+ await writeEntry(table, sources, sourcesFile, options, computeDuration);
54
+ }
55
+ async function loadEntry(table, cached, sources, sourcesFile, options) {
56
+ if (cached.formatVersion !== OSM_CACHE_FORMAT_VERSION ||
57
+ typeof cached.file !== "string" ||
58
+ !existsSync(cached.file)) {
59
+ removeEntry(options.id, cached, sources, sourcesFile);
60
+ console.log("The matching processed cache entry was incomplete or missing and was removed. Fresh data is being requested.");
61
+ return false;
62
+ }
63
+ const size = statSync(cached.file).size;
64
+ options.verbose && console.log(`Processed cache hit.\nLoading ${formatBytes(size)} from the GeoParquet cache; Osmium will not run.`);
65
+ const started = Date.now();
66
+ try {
67
+ await queryDB(table, `INSTALL spatial;
68
+ LOAD spatial;
69
+ SET geometry_always_xy = true;
70
+ CREATE OR REPLACE TABLE ${quoteIdentifier(table.name)} AS
71
+ SELECT * FROM read_parquet(${parseValue(cached.file)});`, mergeOptions(table, {
72
+ table: table.name,
73
+ method: "loadOpenStreetMap()",
74
+ parameters: options.parameters,
75
+ }));
76
+ table.sdb.spatialLoaded = true;
77
+ if (options.verbose) {
78
+ const rows = await getRowCount(table, options.parameters);
79
+ console.log(`Loaded ${rows.toLocaleString("en-US")} rows in ${prettyDuration(started)}.`);
80
+ if (cached.duration !== undefined) {
81
+ table.sdb.cacheTimeSaved += cached.duration - (Date.now() - started);
82
+ }
83
+ }
84
+ return true;
85
+ }
86
+ catch {
87
+ removeEntry(options.id, cached, sources, sourcesFile);
88
+ console.log("The matching processed cache entry was corrupt or unusable and was removed. Fresh data is being requested.");
89
+ return false;
90
+ }
91
+ }
92
+ async function writeEntry(table, sources, sourcesFile, options, computeDuration) {
93
+ mkdirSync(CACHE_DIRECTORY, { recursive: true });
94
+ const file = `${CACHE_DIRECTORY}/${options.id}.geoparquet`;
95
+ const temporaryFile = `${file}.${crypto.randomUUID()}.tmp.geoparquet`;
96
+ options.verbose && console.log("Writing processed GeoParquet cache...");
97
+ const started = Date.now();
98
+ try {
99
+ await queryDB(table, `COPY ${quoteIdentifier(table.name)} TO ${parseValue(temporaryFile)}
100
+ WITH (FORMAT PARQUET);`, mergeOptions(table, {
101
+ table: table.name,
102
+ method: "loadOpenStreetMap()",
103
+ parameters: options.parameters,
104
+ }));
105
+ renameSync(temporaryFile, file);
106
+ }
107
+ catch (error) {
108
+ rmSync(temporaryFile, { force: true });
109
+ throw error;
110
+ }
111
+ const creation = Date.now();
112
+ sources[options.id] = {
113
+ file,
114
+ creation,
115
+ duration: computeDuration,
16
116
  formatVersion: OSM_CACHE_FORMAT_VERSION,
17
- id,
18
- method: "loadOSM()",
19
- load: async (file) => {
20
- await queryDB(table, `INSTALL spatial;
21
- LOAD spatial;
22
- SET geometry_always_xy = true;
23
- CREATE OR REPLACE TABLE ${quoteIdentifier(table.name)} AS
24
- SELECT * FROM read_parquet(${parseValue(file)});`, mergeOptions(table, {
25
- table: table.name,
26
- method: "loadOSM()",
27
- parameters,
28
- }));
29
- table.sdb.spatialLoaded = true;
30
- },
31
- compute,
32
- write: async (file) => {
33
- await queryDB(table, `COPY ${quoteIdentifier(table.name)} TO ${parseValue(file)}
34
- WITH (FORMAT PARQUET);`, mergeOptions(table, {
35
- table: table.name,
36
- method: "loadOSM()",
37
- parameters,
38
- }));
39
- },
40
- });
117
+ };
118
+ try {
119
+ writeSources(sourcesFile, sources);
120
+ }
121
+ catch (error) {
122
+ rmSync(file, { force: true });
123
+ delete sources[options.id];
124
+ throw error;
125
+ }
126
+ if (options.verbose) {
127
+ const size = statSync(file).size;
128
+ console.log(`Wrote ${formatBytes(size)} in ${prettyDuration(started)}.`);
129
+ table.sdb.cacheTimeWriting += Date.now() - started;
130
+ }
131
+ }
132
+ async function getRowCount(table, parameters) {
133
+ const data = await queryDB(table, `SELECT count(*)::DOUBLE AS count FROM ${quoteIdentifier(table.name)};`, mergeOptions(table, {
134
+ table: table.name,
135
+ method: "loadOpenStreetMap()",
136
+ parameters,
137
+ returnData: true,
138
+ }));
139
+ const count = data?.[0]?.count;
140
+ return typeof count === "number" ? count : Number(count ?? 0);
141
+ }
142
+ function removeEntry(id, source, sources, sourcesFile) {
143
+ if (typeof source.file === "string")
144
+ rmSync(source.file, { force: true });
145
+ delete sources[id];
146
+ writeSources(sourcesFile, sources);
147
+ }
148
+ function readSources(sourcesFile) {
149
+ if (!existsSync(sourcesFile))
150
+ return {};
151
+ try {
152
+ const value = JSON.parse(readFileSync(sourcesFile, "utf-8"));
153
+ return typeof value === "object" && value !== null
154
+ ? value
155
+ : {};
156
+ }
157
+ catch {
158
+ rmSync(sourcesFile, { force: true });
159
+ console.log("The OpenStreetMap cache index was corrupt and was removed. Fresh data is being requested.");
160
+ return {};
161
+ }
162
+ }
163
+ function isOsmCacheSource(value) {
164
+ if (typeof value !== "object" || value === null)
165
+ return false;
166
+ const source = value;
167
+ return typeof source.file === "string" &&
168
+ typeof source.creation === "number" &&
169
+ Number.isFinite(source.creation) &&
170
+ typeof source.formatVersion === "string" &&
171
+ (source.duration === undefined ||
172
+ (typeof source.duration === "number" &&
173
+ Number.isFinite(source.duration)));
174
+ }
175
+ function writeSources(sourcesFile, sources) {
176
+ mkdirSync(CACHE_DIRECTORY, { recursive: true });
177
+ const temporaryFile = `${sourcesFile}.${crypto.randomUUID()}.tmp`;
178
+ try {
179
+ writeFileSync(temporaryFile, JSON.stringify(sources));
180
+ renameSync(temporaryFile, sourcesFile);
181
+ }
182
+ catch (error) {
183
+ rmSync(temporaryFile, { force: true });
184
+ throw error;
185
+ }
41
186
  }
@@ -1,9 +1,18 @@
1
1
  export type OsmFileSuffix = ".osm" | ".osm.pbf";
2
2
  export declare function getOsmFileSuffix(file: string): OsmFileSuffix | null;
3
3
  export declare function isRemoteOsmUrl(file: string): boolean;
4
+ export declare function normalizeRemoteOsmUrl(file: string): string;
5
+ export declare function sanitizeOsmUrl(source: string): string;
4
6
  export declare function downloadOsmToTemporaryFile(url: string, options: {
5
7
  suffix: OsmFileSuffix;
6
8
  prefix?: string;
7
9
  request?: RequestInit;
10
+ retries?: number;
11
+ retryDelay?: number;
12
+ timeout?: number;
13
+ overpass?: boolean;
14
+ verbose?: boolean;
15
+ wait?: (milliseconds: number) => Promise<void>;
8
16
  }): Promise<string>;
17
+ export declare function formatBytes(bytes: number): string;
9
18
  //# sourceMappingURL=osmFiles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"osmFiles.d.ts","sourceRoot":"","sources":["../../src/helpers/osmFiles.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,UAAU,CAAC;AAEhD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAUnE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOpD;AAED,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;IACP,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB,GACA,OAAO,CAAC,MAAM,CAAC,CAyCjB"}
1
+ {"version":3,"file":"osmFiles.d.ts","sourceRoot":"","sources":["../../src/helpers/osmFiles.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,UAAU,CAAC;AAShD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAMnE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOpD;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAe1D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAmBrD;AAED,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;IACP,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD,GACA,OAAO,CAAC,MAAM,CAAC,CAmHjB;AAgMD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWjD"}
@@ -1,17 +1,16 @@
1
1
  import crypto from "node:crypto";
2
2
  import { createWriteStream, mkdirSync } from "node:fs";
3
3
  import { rm } from "node:fs/promises";
4
- import { pipeline } from "node:stream/promises";
5
4
  import { Readable } from "node:stream";
5
+ import { pipeline } from "node:stream/promises";
6
+ import prettyDuration from "./prettyDuration.js";
6
7
  export function getOsmFileSuffix(file) {
7
8
  const comparable = isRemoteOsmUrl(file) ? new URL(file).pathname : file;
8
9
  const lower = comparable.toLowerCase();
9
- if (lower.endsWith(".osm.pbf")) {
10
+ if (lower.endsWith(".osm.pbf"))
10
11
  return ".osm.pbf";
11
- }
12
- if (lower.endsWith(".osm")) {
12
+ if (lower.endsWith(".osm"))
13
13
  return ".osm";
14
- }
15
14
  return null;
16
15
  }
17
16
  export function isRemoteOsmUrl(file) {
@@ -23,30 +22,242 @@ export function isRemoteOsmUrl(file) {
23
22
  return false;
24
23
  }
25
24
  }
26
- export async function downloadOsmToTemporaryFile(url, options) {
27
- const temporaryDirectory = ".sda-cache/tmp";
28
- mkdirSync(temporaryDirectory, { recursive: true });
29
- const prefix = options.prefix === undefined ? "remote" : options.prefix;
30
- const temporaryFile = `${temporaryDirectory}/${prefix}.${crypto.randomUUID()}${options.suffix}`;
31
- const response = await fetch(url, options.request);
32
- if (!response.ok) {
33
- const responseText = (await response.text()).slice(0, 500).trim();
34
- throw new Error(`Could not download OSM data from ${JSON.stringify(url)}: HTTP ${response.status} ${response.statusText}${responseText.length > 0 ? `\n${responseText}` : ""}`);
25
+ export function normalizeRemoteOsmUrl(file) {
26
+ let url;
27
+ try {
28
+ url = new URL(file);
29
+ }
30
+ catch {
31
+ throw new Error("loadOpenStreetMap() remote sources must be valid HTTP or HTTPS URLs.");
35
32
  }
36
- if (response.body === null) {
37
- throw new Error(`Could not download OSM data from ${JSON.stringify(url)}: the response body is empty.`);
33
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
34
+ throw new Error("loadOpenStreetMap() remote sources must use an HTTP or HTTPS URL.");
38
35
  }
36
+ return url.href;
37
+ }
38
+ export function sanitizeOsmUrl(source) {
39
+ let url;
39
40
  try {
40
- await pipeline(Readable.fromWeb(response.body), createWriteStream(temporaryFile, { flags: "wx" }));
41
+ url = new URL(source);
42
+ }
43
+ catch {
44
+ return source;
45
+ }
46
+ url.username = "";
47
+ url.password = "";
48
+ for (const key of [...url.searchParams.keys()]) {
49
+ if (/(?:api[-_]?key|auth|credential|password|secret|signature|token)/i.test(key)) {
50
+ url.searchParams.set(key, "[REDACTED]");
51
+ }
41
52
  }
42
- catch (error) {
53
+ return url.href;
54
+ }
55
+ export async function downloadOsmToTemporaryFile(url, options) {
56
+ const temporaryDirectory = ".sda-cache/tmp";
57
+ mkdirSync(temporaryDirectory, { recursive: true });
58
+ const prefix = options.prefix === undefined ? "remote" : options.prefix;
59
+ const retries = options.retries ?? 3;
60
+ const retryDelay = options.retryDelay ?? 5;
61
+ const totalAttempts = retries + 1;
62
+ const sanitizedSource = sanitizeOsmUrl(url);
63
+ const wait = options.wait ?? waitFor;
64
+ let totalDelay = 0;
65
+ let lastFailure;
66
+ for (let attempt = 1; attempt <= totalAttempts; attempt++) {
67
+ const temporaryFile = `${temporaryDirectory}/${prefix}.${crypto.randomUUID()}${options.suffix}`;
68
+ const start = Date.now();
69
+ options.verbose && console.log(`${options.overpass ? "Querying Overpass" : "Downloading OSM data"} (attempt ${attempt} of ${totalAttempts})...`);
43
70
  try {
44
- await rm(temporaryFile, { force: true });
71
+ const response = await fetchWithTimeout(url, options.request, options.timeout);
72
+ if (!response.ok) {
73
+ const failure = classifyHttpFailure(response, options.overpass ?? false);
74
+ if (!failure.retryable) {
75
+ const responseText = (await response.text()).slice(0, 500).trim();
76
+ if (responseText.length > 0) {
77
+ failure.reason += ` Response: ${responseText}`;
78
+ }
79
+ }
80
+ else {
81
+ await response.body?.cancel();
82
+ }
83
+ throw failure;
84
+ }
85
+ if (response.body === null) {
86
+ throw {
87
+ reason: "The response body was empty.",
88
+ retryable: true,
89
+ explanation: "an empty response can be caused by an interrupted transfer",
90
+ };
91
+ }
92
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(temporaryFile, { flags: "wx" }));
93
+ if (options.verbose) {
94
+ const size = (await Deno.stat(temporaryFile)).size;
95
+ console.log(`${options.overpass ? "Received" : "Downloaded"} ${formatBytes(size)} in ${prettyDuration(start)}.`);
96
+ }
97
+ return temporaryFile;
45
98
  }
46
- catch {
47
- // Preserve the download error.
99
+ catch (error) {
100
+ await rm(temporaryFile, { force: true }).catch(() => undefined);
101
+ lastFailure = isRetryFailure(error)
102
+ ? error
103
+ : classifyFetchFailure(error, options.timeout);
104
+ const canRetry = lastFailure.retryable && attempt < totalAttempts;
105
+ logAttemptFailure(sanitizedSource, attempt, totalAttempts, lastFailure, canRetry);
106
+ if (!canRetry) {
107
+ throw new Error(finalRequestError(attempt, retries, totalDelay, lastFailure), { cause: error });
108
+ }
109
+ const calculatedDelay = retryDelay * 2 ** (attempt - 1);
110
+ const delay = Math.max(calculatedDelay, lastFailure.retryAfterSeconds ?? 0);
111
+ logRetryDelay(delay, calculatedDelay, lastFailure.retryAfterSeconds, attempt, retries, retryDelay);
112
+ totalDelay += delay;
113
+ await wait(delay * 1000);
48
114
  }
49
- throw error;
50
115
  }
51
- return temporaryFile;
116
+ throw new Error(finalRequestError(totalAttempts, retries, totalDelay, lastFailure ?? {
117
+ reason: "Unknown request failure.",
118
+ retryable: false,
119
+ explanation: "the failure could not be classified",
120
+ }));
121
+ }
122
+ async function fetchWithTimeout(url, request, timeout) {
123
+ const timeoutSignal = timeout === undefined
124
+ ? undefined
125
+ : AbortSignal.timeout(timeout * 1000);
126
+ const requestSignal = request?.signal ?? undefined;
127
+ const signal = timeoutSignal === undefined
128
+ ? requestSignal
129
+ : requestSignal === undefined
130
+ ? timeoutSignal
131
+ : AbortSignal.any([timeoutSignal, requestSignal]);
132
+ return await fetch(url, { ...request, signal });
133
+ }
134
+ function classifyHttpFailure(response, overpass) {
135
+ const status = `HTTP ${response.status}${response.statusText.length > 0 ? ` ${response.statusText}` : ""}`;
136
+ const retryAfterSeconds = parseRetryAfter(response.headers.get("retry-after"));
137
+ if (response.status === 408) {
138
+ return {
139
+ reason: status,
140
+ retryable: true,
141
+ explanation: "HTTP 408 indicates a transient request timeout",
142
+ retryAfterSeconds,
143
+ };
144
+ }
145
+ if (response.status === 429) {
146
+ return {
147
+ reason: status,
148
+ retryable: true,
149
+ explanation: "HTTP 429 indicates temporary rate limiting",
150
+ retryAfterSeconds,
151
+ };
152
+ }
153
+ if (response.status >= 500 && response.status <= 599) {
154
+ return {
155
+ reason: status,
156
+ retryable: true,
157
+ explanation: "it is a server error",
158
+ retryAfterSeconds,
159
+ };
160
+ }
161
+ if (overpass && response.status === 406) {
162
+ return {
163
+ reason: status,
164
+ retryable: true,
165
+ explanation: "Overpass public instances use HTTP 406 for temporary resource limits",
166
+ retryAfterSeconds,
167
+ };
168
+ }
169
+ return {
170
+ reason: status,
171
+ retryable: false,
172
+ explanation: `HTTP ${response.status} is not considered a transient failure`,
173
+ };
174
+ }
175
+ function classifyFetchFailure(error, timeout) {
176
+ const message = error instanceof Error ? error.message : String(error);
177
+ const timedOut = error instanceof DOMException &&
178
+ (error.name === "TimeoutError" || error.name === "AbortError") &&
179
+ timeout !== undefined;
180
+ return {
181
+ reason: timedOut
182
+ ? `The request timed out after ${timeout} seconds.`
183
+ : message,
184
+ retryable: true,
185
+ explanation: timedOut
186
+ ? "a configured request timeout is retryable"
187
+ : "fetch and transfer errors are treated as transient network failures",
188
+ };
189
+ }
190
+ function isRetryFailure(error) {
191
+ if (typeof error !== "object" || error === null)
192
+ return false;
193
+ const candidate = error;
194
+ return typeof candidate.reason === "string" &&
195
+ typeof candidate.retryable === "boolean" &&
196
+ typeof candidate.explanation === "string";
197
+ }
198
+ function parseRetryAfter(value) {
199
+ if (value === null)
200
+ return undefined;
201
+ const trimmed = value.trim();
202
+ if (/^\d+$/.test(trimmed))
203
+ return Number(trimmed);
204
+ const date = Date.parse(trimmed);
205
+ if (Number.isNaN(date))
206
+ return undefined;
207
+ return Math.max(0, Math.ceil((date - Date.now()) / 1000));
208
+ }
209
+ function logAttemptFailure(source, attempt, totalAttempts, failure, canRetry) {
210
+ console.log(`loadOpenStreetMap(): request attempt ${attempt} of ${totalAttempts} failed.\n` +
211
+ `Source: ${source}\n` +
212
+ `Reason: ${failure.reason}\n` +
213
+ `${canRetry
214
+ ? `This failure is retryable because ${failure.explanation}.`
215
+ : `Not retrying because ${failure.retryable
216
+ ? totalAttempts === 1
217
+ ? "no retries were configured"
218
+ : "the configured retries have been exhausted"
219
+ : failure.explanation}.`}`);
220
+ }
221
+ function logRetryDelay(delay, calculatedDelay, retryAfter, attempt, retries, retryDelay) {
222
+ if (retryAfter !== undefined && retryAfter > calculatedDelay) {
223
+ console.log(`Retrying in ${formatSeconds(delay)} because the server returned Retry-After: ${retryAfter}.\n` +
224
+ `The calculated retry delay was ${formatSeconds(calculatedDelay)}; the longer server-requested delay is being used.`);
225
+ return;
226
+ }
227
+ console.log(`Retrying in ${formatSeconds(delay)} (retry ${attempt} of ${retries}; configured base delay: ${formatSeconds(retryDelay)}).`);
228
+ }
229
+ function finalRequestError(attempts, retries, totalDelay, failure) {
230
+ const exhausted = failure.retryable && attempts === retries + 1;
231
+ return `loadOpenStreetMap(): request failed after ${attempts} ${attempts === 1 ? "attempt" : "attempts"}.\n${exhausted
232
+ ? retries === 0
233
+ ? "No retries were configured."
234
+ : `All ${retries} configured ${retries === 1 ? "retry was" : "retries were"} exhausted.`
235
+ : "No further retry was attempted because the failure is nonretryable."}\nTotal retry delay: ${formatSeconds(totalDelay)}.\n` +
236
+ `Last failure: ${failure.reason}\n` +
237
+ `No further retry occurred because ${exhausted
238
+ ? retries === 0
239
+ ? "no retries were configured"
240
+ : "the configured retries were exhausted"
241
+ : failure.explanation}.\n` +
242
+ "No invalid cached data was returned.";
243
+ }
244
+ function formatSeconds(seconds) {
245
+ return `${seconds} ${seconds === 1 ? "second" : "seconds"}`;
246
+ }
247
+ export function formatBytes(bytes) {
248
+ if (bytes < 1024)
249
+ return `${bytes} ${bytes === 1 ? "byte" : "bytes"}`;
250
+ const units = ["KB", "MB", "GB", "TB"];
251
+ let value = bytes;
252
+ let unit = units[0];
253
+ for (const candidate of units) {
254
+ value /= 1024;
255
+ unit = candidate;
256
+ if (value < 1024)
257
+ break;
258
+ }
259
+ return `${value.toFixed(1)} ${unit}`;
260
+ }
261
+ function waitFor(milliseconds) {
262
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
52
263
  }
@@ -1 +1 @@
1
- {"version":3,"file":"loadGeoData.d.ts","sourceRoot":"","sources":["../../src/methods/loadGeoData.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAWvD,KAAK,kBAAkB,GAAG;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,WAAW,CACjC,WAAW,EAAE,WAAW,EACxB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,kBAAuB,QAYjC"}
1
+ {"version":3,"file":"loadGeoData.d.ts","sourceRoot":"","sources":["../../src/methods/loadGeoData.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAIvD,KAAK,kBAAkB,GAAG;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,WAAW,CACjC,WAAW,EAAE,WAAW,EACxB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,kBAAuB,QAiBjC"}
@@ -5,12 +5,12 @@ import parseValue from "../helpers/parseValue.js";
5
5
  import queryDB from "../helpers/queryDB.js";
6
6
  import queueOp from "../helpers/queueOp.js";
7
7
  import removeColumnsNow from "../helpers/removeColumnsNow.js";
8
- import { rmSync } from "node:fs";
9
- import SDAError from "../class/SDAError.js";
10
- import loadOsmFile from "../helpers/loadOsmFile.js";
11
8
  import { recordCacheTableReferences } from "../helpers/cacheTableDependencies.js";
12
- import { downloadOsmToTemporaryFile, getOsmFileSuffix, isRemoteOsmUrl, } from "../helpers/osmFiles.js";
9
+ import { getOsmFileSuffix } from "../helpers/osmFiles.js";
13
10
  export default function loadGeoData(simpleTable, file, options = {}) {
11
+ if (getOsmFileSuffix(file) !== null) {
12
+ throw new Error("loadGeoData() does not support .osm or .osm.pbf files. Use loadOpenStreetMap() instead.");
13
+ }
14
14
  options = structuredClone(options);
15
15
  if (options.conditions !== undefined) {
16
16
  recordCacheTableReferences(simpleTable, [options.conditions]);
@@ -23,41 +23,6 @@ export default function loadGeoData(simpleTable, file, options = {}) {
23
23
  });
24
24
  }
25
25
  async function executeLoadGeoData(simpleTable, file, options) {
26
- const osmSuffix = getOsmFileSuffix(file);
27
- if (osmSuffix !== null) {
28
- let temporaryFile;
29
- try {
30
- const localFile = isRemoteOsmUrl(file)
31
- ? temporaryFile = await downloadOsmToTemporaryFile(file, {
32
- suffix: osmSuffix,
33
- request: { headers: { "accept-encoding": "gzip" } },
34
- })
35
- : file;
36
- await loadOsmFile(simpleTable, localFile, {
37
- method: "loadGeoData()",
38
- parameters: { file, options },
39
- columns: options.columns,
40
- conditions: options.conditions,
41
- });
42
- return;
43
- }
44
- catch (error) {
45
- if (error instanceof SDAError) {
46
- throw error;
47
- }
48
- throw new SDAError({
49
- method: "loadGeoData()",
50
- parameters: { file, options },
51
- query: "",
52
- cause: error,
53
- });
54
- }
55
- finally {
56
- if (temporaryFile !== undefined) {
57
- rmSync(temporaryFile, { force: true });
58
- }
59
- }
60
- }
61
26
  const fileExtension = getExtension(file);
62
27
  const selectColumns = options.columns !== undefined &&
63
28
  options.columns.length > 0
@@ -0,0 +1,24 @@
1
+ import type SimpleTable from "../class/SimpleTable.js";
2
+ type BoundingBox = {
3
+ west: number;
4
+ south: number;
5
+ east: number;
6
+ north: number;
7
+ };
8
+ type CommonOptions = {
9
+ cache?: boolean;
10
+ ttl?: number;
11
+ retries?: number;
12
+ retryDelay?: number;
13
+ verbose?: boolean;
14
+ };
15
+ type LoadOpenStreetMapFileOptions = CommonOptions;
16
+ type LoadOpenStreetMapOverpassOptions = CommonOptions & {
17
+ filters: string | [string, string] | [string, string][];
18
+ endpoint?: string;
19
+ timeout?: number;
20
+ };
21
+ export default function loadOpenStreetMap(table: SimpleTable, source: string | BoundingBox, options?: LoadOpenStreetMapFileOptions | LoadOpenStreetMapOverpassOptions): void;
22
+ export declare function generateOverpassQuery(bbox: BoundingBox, filters: string[], timeout?: number): string;
23
+ export {};
24
+ //# sourceMappingURL=loadOpenStreetMap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadOpenStreetMap.d.ts","sourceRoot":"","sources":["../../src/methods/loadOpenStreetMap.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAqBvD,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,KAAK,4BAA4B,GAAG,aAAa,CAAC;AAElD,KAAK,gCAAgC,GAAG,aAAa,GAAG;IACtD,OAAO,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACvC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,GAAG,WAAW,EAC5B,OAAO,GACH,4BAA4B,GAC5B,gCAAqC,GACxC,IAAI,CAcN;AA+SD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,CASR"}