@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,81 +1,193 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
38
5
  Object.defineProperty(exports, "__esModule", { value: true });
39
6
  exports.createOsmCacheId = createOsmCacheId;
40
7
  exports.useProcessedOsmCache = useProcessedOsmCache;
41
- const fileCache_js_1 = __importStar(require("./fileCache.js"));
8
+ const node_crypto_1 = __importDefault(require("node:crypto"));
9
+ const node_fs_1 = require("node:fs");
10
+ const cacheDiagnostics_js_1 = require("./cacheDiagnostics.js");
11
+ const fileCache_js_1 = require("./fileCache.js");
12
+ const osmFiles_js_1 = require("./osmFiles.js");
42
13
  const mergeOptions_js_1 = __importDefault(require("./mergeOptions.js"));
43
14
  const parseValue_js_1 = __importDefault(require("./parseValue.js"));
15
+ const prettyDuration_js_1 = __importDefault(require("./prettyDuration.js"));
44
16
  const queryDB_js_1 = __importDefault(require("./queryDB.js"));
45
17
  const quoteIdentifier_js_1 = __importDefault(require("./quoteIdentifier.js"));
46
18
  const CACHE_DIRECTORY = ".sda-cache/osm";
47
- const OSM_CACHE_FORMAT_VERSION = "osm-geoparquet-v1";
19
+ const OSM_CACHE_FORMAT_VERSION = "osm-geoparquet-v2";
48
20
  function createOsmCacheId(identity) {
49
- return (0, fileCache_js_1.createFileCacheId)("osm", identity);
21
+ return (0, fileCache_js_1.createFileCacheId)("osm", [OSM_CACHE_FORMAT_VERSION, identity]);
22
+ }
23
+ async function useProcessedOsmCache(table, options) {
24
+ if (!options.enabled) {
25
+ options.verbose && console.log("Processed cache disabled. Fresh data will be used without reading or writing cache entries.");
26
+ await options.compute();
27
+ return;
28
+ }
29
+ const sourcesFile = `${CACHE_DIRECTORY}/sources.json`;
30
+ const sources = readSources(sourcesFile);
31
+ const cached = sources[options.id];
32
+ if (cached !== undefined) {
33
+ if (!isOsmCacheSource(cached)) {
34
+ delete sources[options.id];
35
+ writeSources(sourcesFile, sources);
36
+ console.log("The matching processed cache entry was incomplete and was removed. Fresh data is being requested.");
37
+ }
38
+ else {
39
+ const now = Date.now();
40
+ if (options.ttl !== undefined &&
41
+ (0, cacheDiagnostics_js_1.cacheEntryExpired)(cached.creation, options.ttl, now)) {
42
+ const age = now - cached.creation;
43
+ removeEntry(options.id, cached, sources, sourcesFile);
44
+ console.log(`Processed cache entry expired after ${(0, prettyDuration_js_1.default)(0, { end: age })}; ` +
45
+ `the configured TTL is ${(0, prettyDuration_js_1.default)(0, { end: options.ttl * 1000 })}.\n` +
46
+ "The matching cache file was removed. Fresh data is being requested.");
47
+ }
48
+ else if (await loadEntry(table, cached, sources, sourcesFile, options)) {
49
+ return;
50
+ }
51
+ }
52
+ }
53
+ else {
54
+ options.verbose &&
55
+ console.log("No processed cache entry matched this source.");
56
+ }
57
+ const started = Date.now();
58
+ await options.compute();
59
+ const computeDuration = Date.now() - started;
60
+ await writeEntry(table, sources, sourcesFile, options, computeDuration);
50
61
  }
51
- async function useProcessedOsmCache(table, id, enabled, parameters, compute) {
52
- await (0, fileCache_js_1.default)(table, {
53
- directory: CACHE_DIRECTORY,
54
- enabled,
55
- extension: "geoparquet",
62
+ async function loadEntry(table, cached, sources, sourcesFile, options) {
63
+ if (cached.formatVersion !== OSM_CACHE_FORMAT_VERSION ||
64
+ typeof cached.file !== "string" ||
65
+ !(0, node_fs_1.existsSync)(cached.file)) {
66
+ removeEntry(options.id, cached, sources, sourcesFile);
67
+ console.log("The matching processed cache entry was incomplete or missing and was removed. Fresh data is being requested.");
68
+ return false;
69
+ }
70
+ const size = (0, node_fs_1.statSync)(cached.file).size;
71
+ options.verbose && console.log(`Processed cache hit.\nLoading ${(0, osmFiles_js_1.formatBytes)(size)} from the GeoParquet cache; Osmium will not run.`);
72
+ const started = Date.now();
73
+ try {
74
+ await (0, queryDB_js_1.default)(table, `INSTALL spatial;
75
+ LOAD spatial;
76
+ SET geometry_always_xy = true;
77
+ CREATE OR REPLACE TABLE ${(0, quoteIdentifier_js_1.default)(table.name)} AS
78
+ SELECT * FROM read_parquet(${(0, parseValue_js_1.default)(cached.file)});`, (0, mergeOptions_js_1.default)(table, {
79
+ table: table.name,
80
+ method: "loadOpenStreetMap()",
81
+ parameters: options.parameters,
82
+ }));
83
+ table.sdb.spatialLoaded = true;
84
+ if (options.verbose) {
85
+ const rows = await getRowCount(table, options.parameters);
86
+ console.log(`Loaded ${rows.toLocaleString("en-US")} rows in ${(0, prettyDuration_js_1.default)(started)}.`);
87
+ if (cached.duration !== undefined) {
88
+ table.sdb.cacheTimeSaved += cached.duration - (Date.now() - started);
89
+ }
90
+ }
91
+ return true;
92
+ }
93
+ catch {
94
+ removeEntry(options.id, cached, sources, sourcesFile);
95
+ console.log("The matching processed cache entry was corrupt or unusable and was removed. Fresh data is being requested.");
96
+ return false;
97
+ }
98
+ }
99
+ async function writeEntry(table, sources, sourcesFile, options, computeDuration) {
100
+ (0, node_fs_1.mkdirSync)(CACHE_DIRECTORY, { recursive: true });
101
+ const file = `${CACHE_DIRECTORY}/${options.id}.geoparquet`;
102
+ const temporaryFile = `${file}.${node_crypto_1.default.randomUUID()}.tmp.geoparquet`;
103
+ options.verbose && console.log("Writing processed GeoParquet cache...");
104
+ const started = Date.now();
105
+ try {
106
+ await (0, queryDB_js_1.default)(table, `COPY ${(0, quoteIdentifier_js_1.default)(table.name)} TO ${(0, parseValue_js_1.default)(temporaryFile)}
107
+ WITH (FORMAT PARQUET);`, (0, mergeOptions_js_1.default)(table, {
108
+ table: table.name,
109
+ method: "loadOpenStreetMap()",
110
+ parameters: options.parameters,
111
+ }));
112
+ (0, node_fs_1.renameSync)(temporaryFile, file);
113
+ }
114
+ catch (error) {
115
+ (0, node_fs_1.rmSync)(temporaryFile, { force: true });
116
+ throw error;
117
+ }
118
+ const creation = Date.now();
119
+ sources[options.id] = {
120
+ file,
121
+ creation,
122
+ duration: computeDuration,
56
123
  formatVersion: OSM_CACHE_FORMAT_VERSION,
57
- id,
58
- method: "loadOSM()",
59
- load: async (file) => {
60
- await (0, queryDB_js_1.default)(table, `INSTALL spatial;
61
- LOAD spatial;
62
- SET geometry_always_xy = true;
63
- CREATE OR REPLACE TABLE ${(0, quoteIdentifier_js_1.default)(table.name)} AS
64
- SELECT * FROM read_parquet(${(0, parseValue_js_1.default)(file)});`, (0, mergeOptions_js_1.default)(table, {
65
- table: table.name,
66
- method: "loadOSM()",
67
- parameters,
68
- }));
69
- table.sdb.spatialLoaded = true;
70
- },
71
- compute,
72
- write: async (file) => {
73
- await (0, queryDB_js_1.default)(table, `COPY ${(0, quoteIdentifier_js_1.default)(table.name)} TO ${(0, parseValue_js_1.default)(file)}
74
- WITH (FORMAT PARQUET);`, (0, mergeOptions_js_1.default)(table, {
75
- table: table.name,
76
- method: "loadOSM()",
77
- parameters,
78
- }));
79
- },
80
- });
124
+ };
125
+ try {
126
+ writeSources(sourcesFile, sources);
127
+ }
128
+ catch (error) {
129
+ (0, node_fs_1.rmSync)(file, { force: true });
130
+ delete sources[options.id];
131
+ throw error;
132
+ }
133
+ if (options.verbose) {
134
+ const size = (0, node_fs_1.statSync)(file).size;
135
+ console.log(`Wrote ${(0, osmFiles_js_1.formatBytes)(size)} in ${(0, prettyDuration_js_1.default)(started)}.`);
136
+ table.sdb.cacheTimeWriting += Date.now() - started;
137
+ }
138
+ }
139
+ async function getRowCount(table, parameters) {
140
+ const data = await (0, queryDB_js_1.default)(table, `SELECT count(*)::DOUBLE AS count FROM ${(0, quoteIdentifier_js_1.default)(table.name)};`, (0, mergeOptions_js_1.default)(table, {
141
+ table: table.name,
142
+ method: "loadOpenStreetMap()",
143
+ parameters,
144
+ returnData: true,
145
+ }));
146
+ const count = data?.[0]?.count;
147
+ return typeof count === "number" ? count : Number(count ?? 0);
148
+ }
149
+ function removeEntry(id, source, sources, sourcesFile) {
150
+ if (typeof source.file === "string")
151
+ (0, node_fs_1.rmSync)(source.file, { force: true });
152
+ delete sources[id];
153
+ writeSources(sourcesFile, sources);
154
+ }
155
+ function readSources(sourcesFile) {
156
+ if (!(0, node_fs_1.existsSync)(sourcesFile))
157
+ return {};
158
+ try {
159
+ const value = JSON.parse((0, node_fs_1.readFileSync)(sourcesFile, "utf-8"));
160
+ return typeof value === "object" && value !== null
161
+ ? value
162
+ : {};
163
+ }
164
+ catch {
165
+ (0, node_fs_1.rmSync)(sourcesFile, { force: true });
166
+ console.log("The OpenStreetMap cache index was corrupt and was removed. Fresh data is being requested.");
167
+ return {};
168
+ }
169
+ }
170
+ function isOsmCacheSource(value) {
171
+ if (typeof value !== "object" || value === null)
172
+ return false;
173
+ const source = value;
174
+ return typeof source.file === "string" &&
175
+ typeof source.creation === "number" &&
176
+ Number.isFinite(source.creation) &&
177
+ typeof source.formatVersion === "string" &&
178
+ (source.duration === undefined ||
179
+ (typeof source.duration === "number" &&
180
+ Number.isFinite(source.duration)));
181
+ }
182
+ function writeSources(sourcesFile, sources) {
183
+ (0, node_fs_1.mkdirSync)(CACHE_DIRECTORY, { recursive: true });
184
+ const temporaryFile = `${sourcesFile}.${node_crypto_1.default.randomUUID()}.tmp`;
185
+ try {
186
+ (0, node_fs_1.writeFileSync)(temporaryFile, JSON.stringify(sources));
187
+ (0, node_fs_1.renameSync)(temporaryFile, sourcesFile);
188
+ }
189
+ catch (error) {
190
+ (0, node_fs_1.rmSync)(temporaryFile, { force: true });
191
+ throw error;
192
+ }
81
193
  }
@@ -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"}
@@ -5,21 +5,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getOsmFileSuffix = getOsmFileSuffix;
7
7
  exports.isRemoteOsmUrl = isRemoteOsmUrl;
8
+ exports.normalizeRemoteOsmUrl = normalizeRemoteOsmUrl;
9
+ exports.sanitizeOsmUrl = sanitizeOsmUrl;
8
10
  exports.downloadOsmToTemporaryFile = downloadOsmToTemporaryFile;
11
+ exports.formatBytes = formatBytes;
9
12
  const node_crypto_1 = __importDefault(require("node:crypto"));
10
13
  const node_fs_1 = require("node:fs");
11
14
  const promises_1 = require("node:fs/promises");
12
- const promises_2 = require("node:stream/promises");
13
15
  const node_stream_1 = require("node:stream");
16
+ const promises_2 = require("node:stream/promises");
17
+ const prettyDuration_js_1 = __importDefault(require("./prettyDuration.js"));
14
18
  function getOsmFileSuffix(file) {
15
19
  const comparable = isRemoteOsmUrl(file) ? new URL(file).pathname : file;
16
20
  const lower = comparable.toLowerCase();
17
- if (lower.endsWith(".osm.pbf")) {
21
+ if (lower.endsWith(".osm.pbf"))
18
22
  return ".osm.pbf";
19
- }
20
- if (lower.endsWith(".osm")) {
23
+ if (lower.endsWith(".osm"))
21
24
  return ".osm";
22
- }
23
25
  return null;
24
26
  }
25
27
  function isRemoteOsmUrl(file) {
@@ -31,30 +33,242 @@ function isRemoteOsmUrl(file) {
31
33
  return false;
32
34
  }
33
35
  }
34
- async function downloadOsmToTemporaryFile(url, options) {
35
- const temporaryDirectory = ".sda-cache/tmp";
36
- (0, node_fs_1.mkdirSync)(temporaryDirectory, { recursive: true });
37
- const prefix = options.prefix === undefined ? "remote" : options.prefix;
38
- const temporaryFile = `${temporaryDirectory}/${prefix}.${node_crypto_1.default.randomUUID()}${options.suffix}`;
39
- const response = await fetch(url, options.request);
40
- if (!response.ok) {
41
- const responseText = (await response.text()).slice(0, 500).trim();
42
- throw new Error(`Could not download OSM data from ${JSON.stringify(url)}: HTTP ${response.status} ${response.statusText}${responseText.length > 0 ? `\n${responseText}` : ""}`);
36
+ function normalizeRemoteOsmUrl(file) {
37
+ let url;
38
+ try {
39
+ url = new URL(file);
40
+ }
41
+ catch {
42
+ throw new Error("loadOpenStreetMap() remote sources must be valid HTTP or HTTPS URLs.");
43
43
  }
44
- if (response.body === null) {
45
- throw new Error(`Could not download OSM data from ${JSON.stringify(url)}: the response body is empty.`);
44
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
45
+ throw new Error("loadOpenStreetMap() remote sources must use an HTTP or HTTPS URL.");
46
46
  }
47
+ return url.href;
48
+ }
49
+ function sanitizeOsmUrl(source) {
50
+ let url;
47
51
  try {
48
- await (0, promises_2.pipeline)(node_stream_1.Readable.fromWeb(response.body), (0, node_fs_1.createWriteStream)(temporaryFile, { flags: "wx" }));
52
+ url = new URL(source);
53
+ }
54
+ catch {
55
+ return source;
56
+ }
57
+ url.username = "";
58
+ url.password = "";
59
+ for (const key of [...url.searchParams.keys()]) {
60
+ if (/(?:api[-_]?key|auth|credential|password|secret|signature|token)/i.test(key)) {
61
+ url.searchParams.set(key, "[REDACTED]");
62
+ }
49
63
  }
50
- catch (error) {
64
+ return url.href;
65
+ }
66
+ async function downloadOsmToTemporaryFile(url, options) {
67
+ const temporaryDirectory = ".sda-cache/tmp";
68
+ (0, node_fs_1.mkdirSync)(temporaryDirectory, { recursive: true });
69
+ const prefix = options.prefix === undefined ? "remote" : options.prefix;
70
+ const retries = options.retries ?? 3;
71
+ const retryDelay = options.retryDelay ?? 5;
72
+ const totalAttempts = retries + 1;
73
+ const sanitizedSource = sanitizeOsmUrl(url);
74
+ const wait = options.wait ?? waitFor;
75
+ let totalDelay = 0;
76
+ let lastFailure;
77
+ for (let attempt = 1; attempt <= totalAttempts; attempt++) {
78
+ const temporaryFile = `${temporaryDirectory}/${prefix}.${node_crypto_1.default.randomUUID()}${options.suffix}`;
79
+ const start = Date.now();
80
+ options.verbose && console.log(`${options.overpass ? "Querying Overpass" : "Downloading OSM data"} (attempt ${attempt} of ${totalAttempts})...`);
51
81
  try {
52
- await (0, promises_1.rm)(temporaryFile, { force: true });
82
+ const response = await fetchWithTimeout(url, options.request, options.timeout);
83
+ if (!response.ok) {
84
+ const failure = classifyHttpFailure(response, options.overpass ?? false);
85
+ if (!failure.retryable) {
86
+ const responseText = (await response.text()).slice(0, 500).trim();
87
+ if (responseText.length > 0) {
88
+ failure.reason += ` Response: ${responseText}`;
89
+ }
90
+ }
91
+ else {
92
+ await response.body?.cancel();
93
+ }
94
+ throw failure;
95
+ }
96
+ if (response.body === null) {
97
+ throw {
98
+ reason: "The response body was empty.",
99
+ retryable: true,
100
+ explanation: "an empty response can be caused by an interrupted transfer",
101
+ };
102
+ }
103
+ await (0, promises_2.pipeline)(node_stream_1.Readable.fromWeb(response.body), (0, node_fs_1.createWriteStream)(temporaryFile, { flags: "wx" }));
104
+ if (options.verbose) {
105
+ const size = (await Deno.stat(temporaryFile)).size;
106
+ console.log(`${options.overpass ? "Received" : "Downloaded"} ${formatBytes(size)} in ${(0, prettyDuration_js_1.default)(start)}.`);
107
+ }
108
+ return temporaryFile;
53
109
  }
54
- catch {
55
- // Preserve the download error.
110
+ catch (error) {
111
+ await (0, promises_1.rm)(temporaryFile, { force: true }).catch(() => undefined);
112
+ lastFailure = isRetryFailure(error)
113
+ ? error
114
+ : classifyFetchFailure(error, options.timeout);
115
+ const canRetry = lastFailure.retryable && attempt < totalAttempts;
116
+ logAttemptFailure(sanitizedSource, attempt, totalAttempts, lastFailure, canRetry);
117
+ if (!canRetry) {
118
+ throw new Error(finalRequestError(attempt, retries, totalDelay, lastFailure), { cause: error });
119
+ }
120
+ const calculatedDelay = retryDelay * 2 ** (attempt - 1);
121
+ const delay = Math.max(calculatedDelay, lastFailure.retryAfterSeconds ?? 0);
122
+ logRetryDelay(delay, calculatedDelay, lastFailure.retryAfterSeconds, attempt, retries, retryDelay);
123
+ totalDelay += delay;
124
+ await wait(delay * 1000);
56
125
  }
57
- throw error;
58
126
  }
59
- return temporaryFile;
127
+ throw new Error(finalRequestError(totalAttempts, retries, totalDelay, lastFailure ?? {
128
+ reason: "Unknown request failure.",
129
+ retryable: false,
130
+ explanation: "the failure could not be classified",
131
+ }));
132
+ }
133
+ async function fetchWithTimeout(url, request, timeout) {
134
+ const timeoutSignal = timeout === undefined
135
+ ? undefined
136
+ : AbortSignal.timeout(timeout * 1000);
137
+ const requestSignal = request?.signal ?? undefined;
138
+ const signal = timeoutSignal === undefined
139
+ ? requestSignal
140
+ : requestSignal === undefined
141
+ ? timeoutSignal
142
+ : AbortSignal.any([timeoutSignal, requestSignal]);
143
+ return await fetch(url, { ...request, signal });
144
+ }
145
+ function classifyHttpFailure(response, overpass) {
146
+ const status = `HTTP ${response.status}${response.statusText.length > 0 ? ` ${response.statusText}` : ""}`;
147
+ const retryAfterSeconds = parseRetryAfter(response.headers.get("retry-after"));
148
+ if (response.status === 408) {
149
+ return {
150
+ reason: status,
151
+ retryable: true,
152
+ explanation: "HTTP 408 indicates a transient request timeout",
153
+ retryAfterSeconds,
154
+ };
155
+ }
156
+ if (response.status === 429) {
157
+ return {
158
+ reason: status,
159
+ retryable: true,
160
+ explanation: "HTTP 429 indicates temporary rate limiting",
161
+ retryAfterSeconds,
162
+ };
163
+ }
164
+ if (response.status >= 500 && response.status <= 599) {
165
+ return {
166
+ reason: status,
167
+ retryable: true,
168
+ explanation: "it is a server error",
169
+ retryAfterSeconds,
170
+ };
171
+ }
172
+ if (overpass && response.status === 406) {
173
+ return {
174
+ reason: status,
175
+ retryable: true,
176
+ explanation: "Overpass public instances use HTTP 406 for temporary resource limits",
177
+ retryAfterSeconds,
178
+ };
179
+ }
180
+ return {
181
+ reason: status,
182
+ retryable: false,
183
+ explanation: `HTTP ${response.status} is not considered a transient failure`,
184
+ };
185
+ }
186
+ function classifyFetchFailure(error, timeout) {
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ const timedOut = error instanceof DOMException &&
189
+ (error.name === "TimeoutError" || error.name === "AbortError") &&
190
+ timeout !== undefined;
191
+ return {
192
+ reason: timedOut
193
+ ? `The request timed out after ${timeout} seconds.`
194
+ : message,
195
+ retryable: true,
196
+ explanation: timedOut
197
+ ? "a configured request timeout is retryable"
198
+ : "fetch and transfer errors are treated as transient network failures",
199
+ };
200
+ }
201
+ function isRetryFailure(error) {
202
+ if (typeof error !== "object" || error === null)
203
+ return false;
204
+ const candidate = error;
205
+ return typeof candidate.reason === "string" &&
206
+ typeof candidate.retryable === "boolean" &&
207
+ typeof candidate.explanation === "string";
208
+ }
209
+ function parseRetryAfter(value) {
210
+ if (value === null)
211
+ return undefined;
212
+ const trimmed = value.trim();
213
+ if (/^\d+$/.test(trimmed))
214
+ return Number(trimmed);
215
+ const date = Date.parse(trimmed);
216
+ if (Number.isNaN(date))
217
+ return undefined;
218
+ return Math.max(0, Math.ceil((date - Date.now()) / 1000));
219
+ }
220
+ function logAttemptFailure(source, attempt, totalAttempts, failure, canRetry) {
221
+ console.log(`loadOpenStreetMap(): request attempt ${attempt} of ${totalAttempts} failed.\n` +
222
+ `Source: ${source}\n` +
223
+ `Reason: ${failure.reason}\n` +
224
+ `${canRetry
225
+ ? `This failure is retryable because ${failure.explanation}.`
226
+ : `Not retrying because ${failure.retryable
227
+ ? totalAttempts === 1
228
+ ? "no retries were configured"
229
+ : "the configured retries have been exhausted"
230
+ : failure.explanation}.`}`);
231
+ }
232
+ function logRetryDelay(delay, calculatedDelay, retryAfter, attempt, retries, retryDelay) {
233
+ if (retryAfter !== undefined && retryAfter > calculatedDelay) {
234
+ console.log(`Retrying in ${formatSeconds(delay)} because the server returned Retry-After: ${retryAfter}.\n` +
235
+ `The calculated retry delay was ${formatSeconds(calculatedDelay)}; the longer server-requested delay is being used.`);
236
+ return;
237
+ }
238
+ console.log(`Retrying in ${formatSeconds(delay)} (retry ${attempt} of ${retries}; configured base delay: ${formatSeconds(retryDelay)}).`);
239
+ }
240
+ function finalRequestError(attempts, retries, totalDelay, failure) {
241
+ const exhausted = failure.retryable && attempts === retries + 1;
242
+ return `loadOpenStreetMap(): request failed after ${attempts} ${attempts === 1 ? "attempt" : "attempts"}.\n${exhausted
243
+ ? retries === 0
244
+ ? "No retries were configured."
245
+ : `All ${retries} configured ${retries === 1 ? "retry was" : "retries were"} exhausted.`
246
+ : "No further retry was attempted because the failure is nonretryable."}\nTotal retry delay: ${formatSeconds(totalDelay)}.\n` +
247
+ `Last failure: ${failure.reason}\n` +
248
+ `No further retry occurred because ${exhausted
249
+ ? retries === 0
250
+ ? "no retries were configured"
251
+ : "the configured retries were exhausted"
252
+ : failure.explanation}.\n` +
253
+ "No invalid cached data was returned.";
254
+ }
255
+ function formatSeconds(seconds) {
256
+ return `${seconds} ${seconds === 1 ? "second" : "seconds"}`;
257
+ }
258
+ function formatBytes(bytes) {
259
+ if (bytes < 1024)
260
+ return `${bytes} ${bytes === 1 ? "byte" : "bytes"}`;
261
+ const units = ["KB", "MB", "GB", "TB"];
262
+ let value = bytes;
263
+ let unit = units[0];
264
+ for (const candidate of units) {
265
+ value /= 1024;
266
+ unit = candidate;
267
+ if (value < 1024)
268
+ break;
269
+ }
270
+ return `${value.toFixed(1)} ${unit}`;
271
+ }
272
+ function waitFor(milliseconds) {
273
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
60
274
  }
@@ -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"}
@@ -11,12 +11,12 @@ const parseValue_js_1 = __importDefault(require("../helpers/parseValue.js"));
11
11
  const queryDB_js_1 = __importDefault(require("../helpers/queryDB.js"));
12
12
  const queueOp_js_1 = __importDefault(require("../helpers/queueOp.js"));
13
13
  const removeColumnsNow_js_1 = __importDefault(require("../helpers/removeColumnsNow.js"));
14
- const node_fs_1 = require("node:fs");
15
- const SDAError_js_1 = __importDefault(require("../class/SDAError.js"));
16
- const loadOsmFile_js_1 = __importDefault(require("../helpers/loadOsmFile.js"));
17
14
  const cacheTableDependencies_js_1 = require("../helpers/cacheTableDependencies.js");
18
15
  const osmFiles_js_1 = require("../helpers/osmFiles.js");
19
16
  function loadGeoData(simpleTable, file, options = {}) {
17
+ if ((0, osmFiles_js_1.getOsmFileSuffix)(file) !== null) {
18
+ throw new Error("loadGeoData() does not support .osm or .osm.pbf files. Use loadOpenStreetMap() instead.");
19
+ }
20
20
  options = structuredClone(options);
21
21
  if (options.conditions !== undefined) {
22
22
  (0, cacheTableDependencies_js_1.recordCacheTableReferences)(simpleTable, [options.conditions]);
@@ -29,41 +29,6 @@ function loadGeoData(simpleTable, file, options = {}) {
29
29
  });
30
30
  }
31
31
  async function executeLoadGeoData(simpleTable, file, options) {
32
- const osmSuffix = (0, osmFiles_js_1.getOsmFileSuffix)(file);
33
- if (osmSuffix !== null) {
34
- let temporaryFile;
35
- try {
36
- const localFile = (0, osmFiles_js_1.isRemoteOsmUrl)(file)
37
- ? temporaryFile = await (0, osmFiles_js_1.downloadOsmToTemporaryFile)(file, {
38
- suffix: osmSuffix,
39
- request: { headers: { "accept-encoding": "gzip" } },
40
- })
41
- : file;
42
- await (0, loadOsmFile_js_1.default)(simpleTable, localFile, {
43
- method: "loadGeoData()",
44
- parameters: { file, options },
45
- columns: options.columns,
46
- conditions: options.conditions,
47
- });
48
- return;
49
- }
50
- catch (error) {
51
- if (error instanceof SDAError_js_1.default) {
52
- throw error;
53
- }
54
- throw new SDAError_js_1.default({
55
- method: "loadGeoData()",
56
- parameters: { file, options },
57
- query: "",
58
- cause: error,
59
- });
60
- }
61
- finally {
62
- if (temporaryFile !== undefined) {
63
- (0, node_fs_1.rmSync)(temporaryFile, { force: true });
64
- }
65
- }
66
- }
67
32
  const fileExtension = (0, getExtension_js_1.default)(file);
68
33
  const selectColumns = options.columns !== undefined &&
69
34
  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"}