@superrouteca/smap 0.0.0-stage → 1.0.1

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.
@@ -0,0 +1,300 @@
1
+ /*! sMap JS client 1.0.1 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
2
+ /*
3
+ * Runs wherever `fetch` exists: browsers, Node.js 18+, workers; elsewhere (mini programs, older runtimes) pass your
4
+ * own `fetch`. UMD: `window.SMapClient`, `require('smap-client')`, or an AMD module.
5
+ *
6
+ * const client = new SMapClient({ baseUrl: 'https://tile.srsr.ca', getToken: () => myServer.mapToken() });
7
+ * const keyed = new SMapClient({ baseUrl: 'https://tile.srsr.ca', apiKey: 'smk_…' }); // or a map key
8
+ * const tile = await client.tile('basemap', 12, 1143, 1493); // Uint8Array (vector tile)
9
+ * const style = await client.style('light', 'en'); // style document
10
+ *
11
+ * Every request goes to the nearest node that answers (asked of baseUrl, remembered for an hour) and moves to the
12
+ * next one when a node fails.
13
+ */
14
+ (function (root, factory) {
15
+ if (typeof define === 'function' && define.amd) { define([], factory); }
16
+ else if (typeof module === 'object' && module.exports) { module.exports = factory(); }
17
+ else { root.SMapClient = factory(); }
18
+ }(typeof self !== 'undefined' ? self : this, function () {
19
+ 'use strict';
20
+
21
+ var VERSION = '1.0.1';
22
+ var NODE_TTL = 3600 * 1000; // the measured order
23
+ var KNOWN_TTL = 30 * 86400 * 1000; // the last node list received, used when none can be read
24
+
25
+ function unique(list) {
26
+ var seen = {};
27
+ return list.filter(function (u) { if (!u || seen[u]) { return false; } seen[u] = true; return true; });
28
+ }
29
+ var memory = {};
30
+
31
+ function store() {
32
+ try {
33
+ if (typeof localStorage !== 'undefined') { localStorage.getItem('smap'); return localStorage; }
34
+ } catch (e) { /* storage unavailable */ }
35
+ return { getItem: function (k) { return memory[k] || null; }, setItem: function (k, v) { memory[k] = v; } };
36
+ }
37
+
38
+ function decodeBase64Url(s) {
39
+ s = s.replace(/-/g, '+').replace(/_/g, '/');
40
+ while (s.length % 4) { s += '='; }
41
+ if (typeof atob === 'function') { return atob(s); }
42
+ return Buffer.from(s, 'base64').toString('binary');
43
+ }
44
+
45
+ /** Seconds since 1970 when a token expires; 0 when it cannot be read. */
46
+ function tokenExpiry(token) {
47
+ try { return JSON.parse(decodeBase64Url(String(token).split('.')[1])).exp || 0; } catch (e) { return 0; }
48
+ }
49
+
50
+ function SMapError(message, status, url) {
51
+ var e = new Error(message);
52
+ e.name = 'SMapError';
53
+ e.status = status || 0;
54
+ e.url = url || '';
55
+ return e;
56
+ }
57
+
58
+ /**
59
+ * @param {Object} options
60
+ * @param {string} options.baseUrl the public sMap address (e.g. https://tile.srsr.ca)
61
+ * @param {string} [options.token] a map token; or
62
+ * @param {function(): (string|Promise<string>)} [options.getToken] called at start and before each token expires
63
+ * @param {string} [options.apiKey] a map key (`smk_…`); with a token both are sent (token first)
64
+ * @param {string[]} [options.nodes] node URLs known in advance (used while baseUrl is down)
65
+ * @param {boolean} [options.nearest=true] read from the nearest node
66
+ * @param {function} [options.fetch] a fetch implementation (default: the global one)
67
+ * @param {number} [options.timeoutMs=15000] per request
68
+ */
69
+ function SMapClient(options) {
70
+ if (!(this instanceof SMapClient)) { return new SMapClient(options); }
71
+ options = options || {};
72
+ if (!options.baseUrl) { throw SMapError('sMap: baseUrl is required'); }
73
+ this.baseUrl = String(options.baseUrl).replace(/\/$/, '');
74
+ this._fetch = options.fetch || (typeof fetch === 'function' ? fetch.bind(typeof self !== 'undefined' ? self : globalThis) : null);
75
+ if (!this._fetch) { throw SMapError('sMap: no fetch available; pass options.fetch'); }
76
+ this._token = options.token || null;
77
+ this._getToken = options.getToken || null;
78
+ this._apiKey = options.apiKey || null;
79
+ this._nearest = options.nearest !== false;
80
+ this._seeds = (options.nodes || []).map(function (u) { return String(u).replace(/\/$/, ''); });
81
+ this._timeout = options.timeoutMs || 15000;
82
+ this._nodes = null; // ordered: nearest first
83
+ this._picking = null;
84
+ this._tokening = null;
85
+ }
86
+
87
+ SMapClient.version = VERSION;
88
+ SMapClient.tokenExpiry = tokenExpiry;
89
+
90
+ SMapClient.prototype._timed = function (url, init, ms) {
91
+ var ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
92
+ var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, ms) : null;
93
+ init = init || {};
94
+ if (ctrl) { init.signal = ctrl.signal; }
95
+ return this._fetch(url, init).then(function (res) { clearTimeout(timer); return res; }, function (e) { clearTimeout(timer); throw e; });
96
+ };
97
+
98
+ /** The token to send now, asking getToken again two minutes before it expires. */
99
+ SMapClient.prototype.token = function () {
100
+ var self = this;
101
+ var exp = this._token ? tokenExpiry(this._token) : 0;
102
+ var fresh = this._token && (!exp || exp - Date.now() / 1000 > 120);
103
+ if (fresh || !this._getToken) { return Promise.resolve(this._token); }
104
+ if (!this._tokening) {
105
+ this._tokening = Promise.resolve(this._getToken()).then(function (t) {
106
+ self._token = t;
107
+ self._tokening = null;
108
+ return t;
109
+ }, function (e) { self._tokening = null; throw e; });
110
+ }
111
+ return this._tokening;
112
+ };
113
+
114
+ /**
115
+ * The nodes in the order they are tried: nearest first, then the others; baseUrl alone when nearest is off or the
116
+ * list cannot be read.
117
+ *
118
+ * @returns {Promise<string[]>}
119
+ */
120
+ SMapClient.prototype.nodes = function () {
121
+ var self = this;
122
+ if (this._nodes) { return Promise.resolve(this._nodes); }
123
+ if (!this._nearest) { this._nodes = [this.baseUrl]; return Promise.resolve(this._nodes); }
124
+ if (this._picking) { return this._picking; }
125
+ var key = 'smap.nodes:' + this.baseUrl;
126
+ var storage = store();
127
+ var saved = null;
128
+ try { saved = JSON.parse(storage.getItem(key) || 'null'); } catch (e) { saved = null; }
129
+ if (saved && saved.urls && saved.urls.length && Date.now() - saved.at < NODE_TTL) {
130
+ this._nodes = saved.urls;
131
+ return Promise.resolve(this._nodes);
132
+ }
133
+ // Every node this client knows of: the kept list (30 days), the seed list, then baseUrl.
134
+ var kept = saved && saved.known && Date.now() - (saved.knownAt || 0) < KNOWN_TTL ? saved.known : [];
135
+ var known = unique([this.baseUrl].concat(this._seeds, kept));
136
+ this._picking = this._discover(known)
137
+ .then(function (list) {
138
+ var urls = list || unique(self._seeds.concat(kept));
139
+ if (urls.length === 0) { return { order: [self.baseUrl], known: null }; }
140
+ return self._measure(urls).then(function (order) { return { order: order, known: list }; });
141
+ })
142
+ .then(function (r) {
143
+ self._nodes = r.order;
144
+ self._picking = null;
145
+ // An order is remembered for the hour only when a node list was read; a guess made while nothing answered
146
+ // is tried again by the next client instead of being kept.
147
+ var record = { urls: r.known ? r.order : [], at: r.known ? Date.now() : 0, known: r.known || kept, knownAt: r.known ? Date.now() : (saved && saved.knownAt) || 0 };
148
+ try { storage.setItem(key, JSON.stringify(record)); } catch (e) { /* ignore */ }
149
+ return r.order;
150
+ });
151
+ return this._picking;
152
+ };
153
+
154
+ /** Asks each node in turn for the node list (any node answers the whole list); null when none answers. */
155
+ SMapClient.prototype._discover = function (candidates) {
156
+ var self = this;
157
+ function ask(i) {
158
+ if (i >= candidates.length) { return Promise.resolve(null); }
159
+ return self._timed(candidates[i] + '/v1/nodes', {}, 2000)
160
+ .then(function (res) { return res.ok ? res.json() : null; })
161
+ .then(function (body) {
162
+ var urls = ((body && body.nodes) || []).map(function (n) { return String(n.url).replace(/\/$/, ''); });
163
+ return urls.length ? urls : ask(i + 1);
164
+ }, function () { return ask(i + 1); });
165
+ }
166
+ return ask(0);
167
+ };
168
+
169
+ /** Times one small request to each node: fastest first, silent ones after, baseUrl last when not listed. */
170
+ SMapClient.prototype._measure = function (urls) {
171
+ var self = this;
172
+ return Promise.all(urls.map(function (url) {
173
+ var t0 = Date.now();
174
+ return self._timed(url + '/v1/ping', { cache: 'no-store' }, 1500)
175
+ .then(function (res) { return { url: url, ms: res.ok ? Date.now() - t0 : Infinity }; }, function () { return { url: url, ms: Infinity }; });
176
+ })).then(function (timed) {
177
+ timed.sort(function (a, b) { return a.ms - b.ms; });
178
+ var order = timed.map(function (t) { return t.url; });
179
+ if (order.indexOf(self.baseUrl) < 0) { order.push(self.baseUrl); }
180
+ return order;
181
+ });
182
+ };
183
+
184
+ /** Forgets the chosen node order, so the next request measures again. */
185
+ SMapClient.prototype.resetNodes = function () {
186
+ this._nodes = null;
187
+ var key = 'smap.nodes:' + this.baseUrl;
188
+ try {
189
+ var saved = JSON.parse(store().getItem(key) || 'null');
190
+ // The kept node list survives a reset; only the measured order is forgotten.
191
+ store().setItem(key, JSON.stringify(saved ? { urls: [], at: 0, known: saved.known, knownAt: saved.knownAt } : null));
192
+ } catch (e) { /* ignore */ }
193
+ };
194
+
195
+ /**
196
+ * GET `path` from the nearest node that answers, with the token; a node that fails (network error, 5xx) is moved
197
+ * to the end and the next one is tried. 4xx answers are final.
198
+ *
199
+ * @param {string} path starting with /
200
+ * @param {Object} [headers]
201
+ * @returns {Promise<Response>}
202
+ */
203
+ SMapClient.prototype.request = function (path, headers) {
204
+ var self = this;
205
+ return Promise.all([this.nodes(), this.token()]).then(function (r) {
206
+ var nodes = r[0].slice();
207
+ var token = r[1];
208
+ var last = null;
209
+ function attempt(i) {
210
+ if (i >= nodes.length) { throw last || SMapError('sMap: no node answered', 0, path); }
211
+ var h = {};
212
+ for (var k in (headers || {})) { if (Object.prototype.hasOwnProperty.call(headers, k)) { h[k] = headers[k]; } }
213
+ if (token) { h.Authorization = 'Bearer ' + token; }
214
+ if (self._apiKey) { h['X-SMap-Key'] = self._apiKey; }
215
+ var url = nodes[i] + path;
216
+ return self._timed(url, { headers: h }, self._timeout).then(function (res) {
217
+ if (res.status >= 500) {
218
+ last = SMapError('sMap: HTTP ' + res.status, res.status, url);
219
+ self._demote(nodes[i]);
220
+ return attempt(i + 1);
221
+ }
222
+ if (!res.ok && res.status !== 206) {
223
+ return res.text().then(function (text) { throw SMapError('sMap: ' + (text || ('HTTP ' + res.status)), res.status, url); });
224
+ }
225
+ return res;
226
+ }, function (e) {
227
+ last = SMapError('sMap: ' + (e && e.message ? e.message : 'network error'), 0, url);
228
+ self._demote(nodes[i]);
229
+ return attempt(i + 1);
230
+ });
231
+ }
232
+ return attempt(0);
233
+ });
234
+ };
235
+
236
+ SMapClient.prototype._demote = function (url) {
237
+ if (!this._nodes || this._nodes.length < 2) { return; }
238
+ var i = this._nodes.indexOf(url);
239
+ if (i >= 0) { this._nodes.splice(i, 1); this._nodes.push(url); }
240
+ };
241
+
242
+ function bytes(res) { return res.arrayBuffer().then(function (b) { return new Uint8Array(b); }); }
243
+
244
+ // A map key also goes in the query of styles and TileJSON: the service carries it into their tile URLs, so a
245
+ // renderer given the document reads its tiles with the key.
246
+ SMapClient.prototype._withKey = function (path) {
247
+ if (!this._apiKey) { return path; }
248
+ return path + (path.indexOf('?') >= 0 ? '&' : '?') + 'key=' + encodeURIComponent(this._apiKey);
249
+ };
250
+
251
+ /**
252
+ * A style document ('light', 'white', 'grayscale', 'dark', 'black') with labels in `language`.
253
+ * Its tile URLs point at the node that answered.
254
+ */
255
+ SMapClient.prototype.style = function (flavor, language) {
256
+ var q = language ? '?lang=' + encodeURIComponent(language) : '';
257
+ return this.request(this._withKey('/v1/styles/' + encodeURIComponent(flavor || 'light') + q)).then(function (r) { return r.json(); });
258
+ };
259
+
260
+ /** TileJSON of a source (bounds, zooms, layers, tile URL template). */
261
+ SMapClient.prototype.tileJSON = function (source) {
262
+ return this.request(this._withKey('/v1/tiles/' + encodeURIComponent(source || 'basemap'))).then(function (r) { return r.json(); });
263
+ };
264
+
265
+ /** One vector tile as bytes; null where the source has no tile (204 / 404). */
266
+ SMapClient.prototype.tile = function (source, z, x, y) {
267
+ var path = '/v1/tiles/' + encodeURIComponent(source) + '/' + (z | 0) + '/' + (x | 0) + '/' + (y | 0);
268
+ return this.request(path).then(function (r) { return r.status === 204 ? null : bytes(r); }, function (e) {
269
+ if (e && e.status === 404) { return null; }
270
+ throw e;
271
+ });
272
+ };
273
+
274
+ /** Raw bytes `[start, end]` (inclusive) of a source's archive, where your access allows raw reads. */
275
+ SMapClient.prototype.range = function (source, start, end) {
276
+ return this.request('/' + encodeURIComponent(source) + '.pmtiles', { Range: 'bytes=' + start + '-' + end }).then(bytes);
277
+ };
278
+
279
+ /** A font glyph range (`0-255.pbf` style), for renderers that load fonts themselves. */
280
+ SMapClient.prototype.glyphs = function (fontstack, range) {
281
+ return this.request('/v1/fonts/' + encodeURIComponent(fontstack) + '/' + range + '.pbf').then(bytes);
282
+ };
283
+
284
+ /**
285
+ * Headers and URL rewriting for a renderer that fetches by itself: returns the URL unchanged plus the
286
+ * Authorization (and map key) headers to add for sMap URLs; none for any other host.
287
+ */
288
+ SMapClient.prototype.authorize = function (url) {
289
+ var self = this;
290
+ return this.token().then(function (token) {
291
+ var ours = url.indexOf(self.baseUrl) === 0 || self._seeds.concat(self._nodes || []).some(function (n) { return url.indexOf(n) === 0; });
292
+ var headers = {};
293
+ if (ours && token) { headers.Authorization = 'Bearer ' + token; }
294
+ if (ours && self._apiKey) { headers['X-SMap-Key'] = self._apiKey; }
295
+ return { url: url, headers: headers };
296
+ });
297
+ };
298
+
299
+ return SMapClient;
300
+ }));
@@ -0,0 +1,308 @@
1
+ /* Generated by js/build.mjs from js/src (see SOURCE.json). Do not edit. */
2
+ var smapClientModule = { exports: {} };
3
+ (function (module, exports, define) {
4
+ /*! sMap JS client 1.0.1 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
5
+ /*
6
+ * Runs wherever `fetch` exists: browsers, Node.js 18+, workers; elsewhere (mini programs, older runtimes) pass your
7
+ * own `fetch`. UMD: `window.SMapClient`, `require('smap-client')`, or an AMD module.
8
+ *
9
+ * const client = new SMapClient({ baseUrl: 'https://tile.srsr.ca', getToken: () => myServer.mapToken() });
10
+ * const keyed = new SMapClient({ baseUrl: 'https://tile.srsr.ca', apiKey: 'smk_…' }); // or a map key
11
+ * const tile = await client.tile('basemap', 12, 1143, 1493); // Uint8Array (vector tile)
12
+ * const style = await client.style('light', 'en'); // style document
13
+ *
14
+ * Every request goes to the nearest node that answers (asked of baseUrl, remembered for an hour) and moves to the
15
+ * next one when a node fails.
16
+ */
17
+ (function (root, factory) {
18
+ if (typeof define === 'function' && define.amd) { define([], factory); }
19
+ else if (typeof module === 'object' && module.exports) { module.exports = factory(); }
20
+ else { root.SMapClient = factory(); }
21
+ }(typeof self !== 'undefined' ? self : this, function () {
22
+ 'use strict';
23
+
24
+ var VERSION = '1.0.1';
25
+ var NODE_TTL = 3600 * 1000; // the measured order
26
+ var KNOWN_TTL = 30 * 86400 * 1000; // the last node list received, used when none can be read
27
+
28
+ function unique(list) {
29
+ var seen = {};
30
+ return list.filter(function (u) { if (!u || seen[u]) { return false; } seen[u] = true; return true; });
31
+ }
32
+ var memory = {};
33
+
34
+ function store() {
35
+ try {
36
+ if (typeof localStorage !== 'undefined') { localStorage.getItem('smap'); return localStorage; }
37
+ } catch (e) { /* storage unavailable */ }
38
+ return { getItem: function (k) { return memory[k] || null; }, setItem: function (k, v) { memory[k] = v; } };
39
+ }
40
+
41
+ function decodeBase64Url(s) {
42
+ s = s.replace(/-/g, '+').replace(/_/g, '/');
43
+ while (s.length % 4) { s += '='; }
44
+ if (typeof atob === 'function') { return atob(s); }
45
+ return Buffer.from(s, 'base64').toString('binary');
46
+ }
47
+
48
+ /** Seconds since 1970 when a token expires; 0 when it cannot be read. */
49
+ function tokenExpiry(token) {
50
+ try { return JSON.parse(decodeBase64Url(String(token).split('.')[1])).exp || 0; } catch (e) { return 0; }
51
+ }
52
+
53
+ function SMapError(message, status, url) {
54
+ var e = new Error(message);
55
+ e.name = 'SMapError';
56
+ e.status = status || 0;
57
+ e.url = url || '';
58
+ return e;
59
+ }
60
+
61
+ /**
62
+ * @param {Object} options
63
+ * @param {string} options.baseUrl the public sMap address (e.g. https://tile.srsr.ca)
64
+ * @param {string} [options.token] a map token; or
65
+ * @param {function(): (string|Promise<string>)} [options.getToken] called at start and before each token expires
66
+ * @param {string} [options.apiKey] a map key (`smk_…`); with a token both are sent (token first)
67
+ * @param {string[]} [options.nodes] node URLs known in advance (used while baseUrl is down)
68
+ * @param {boolean} [options.nearest=true] read from the nearest node
69
+ * @param {function} [options.fetch] a fetch implementation (default: the global one)
70
+ * @param {number} [options.timeoutMs=15000] per request
71
+ */
72
+ function SMapClient(options) {
73
+ if (!(this instanceof SMapClient)) { return new SMapClient(options); }
74
+ options = options || {};
75
+ if (!options.baseUrl) { throw SMapError('sMap: baseUrl is required'); }
76
+ this.baseUrl = String(options.baseUrl).replace(/\/$/, '');
77
+ this._fetch = options.fetch || (typeof fetch === 'function' ? fetch.bind(typeof self !== 'undefined' ? self : globalThis) : null);
78
+ if (!this._fetch) { throw SMapError('sMap: no fetch available; pass options.fetch'); }
79
+ this._token = options.token || null;
80
+ this._getToken = options.getToken || null;
81
+ this._apiKey = options.apiKey || null;
82
+ this._nearest = options.nearest !== false;
83
+ this._seeds = (options.nodes || []).map(function (u) { return String(u).replace(/\/$/, ''); });
84
+ this._timeout = options.timeoutMs || 15000;
85
+ this._nodes = null; // ordered: nearest first
86
+ this._picking = null;
87
+ this._tokening = null;
88
+ }
89
+
90
+ SMapClient.version = VERSION;
91
+ SMapClient.tokenExpiry = tokenExpiry;
92
+
93
+ SMapClient.prototype._timed = function (url, init, ms) {
94
+ var ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
95
+ var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, ms) : null;
96
+ init = init || {};
97
+ if (ctrl) { init.signal = ctrl.signal; }
98
+ return this._fetch(url, init).then(function (res) { clearTimeout(timer); return res; }, function (e) { clearTimeout(timer); throw e; });
99
+ };
100
+
101
+ /** The token to send now, asking getToken again two minutes before it expires. */
102
+ SMapClient.prototype.token = function () {
103
+ var self = this;
104
+ var exp = this._token ? tokenExpiry(this._token) : 0;
105
+ var fresh = this._token && (!exp || exp - Date.now() / 1000 > 120);
106
+ if (fresh || !this._getToken) { return Promise.resolve(this._token); }
107
+ if (!this._tokening) {
108
+ this._tokening = Promise.resolve(this._getToken()).then(function (t) {
109
+ self._token = t;
110
+ self._tokening = null;
111
+ return t;
112
+ }, function (e) { self._tokening = null; throw e; });
113
+ }
114
+ return this._tokening;
115
+ };
116
+
117
+ /**
118
+ * The nodes in the order they are tried: nearest first, then the others; baseUrl alone when nearest is off or the
119
+ * list cannot be read.
120
+ *
121
+ * @returns {Promise<string[]>}
122
+ */
123
+ SMapClient.prototype.nodes = function () {
124
+ var self = this;
125
+ if (this._nodes) { return Promise.resolve(this._nodes); }
126
+ if (!this._nearest) { this._nodes = [this.baseUrl]; return Promise.resolve(this._nodes); }
127
+ if (this._picking) { return this._picking; }
128
+ var key = 'smap.nodes:' + this.baseUrl;
129
+ var storage = store();
130
+ var saved = null;
131
+ try { saved = JSON.parse(storage.getItem(key) || 'null'); } catch (e) { saved = null; }
132
+ if (saved && saved.urls && saved.urls.length && Date.now() - saved.at < NODE_TTL) {
133
+ this._nodes = saved.urls;
134
+ return Promise.resolve(this._nodes);
135
+ }
136
+ // Every node this client knows of: the kept list (30 days), the seed list, then baseUrl.
137
+ var kept = saved && saved.known && Date.now() - (saved.knownAt || 0) < KNOWN_TTL ? saved.known : [];
138
+ var known = unique([this.baseUrl].concat(this._seeds, kept));
139
+ this._picking = this._discover(known)
140
+ .then(function (list) {
141
+ var urls = list || unique(self._seeds.concat(kept));
142
+ if (urls.length === 0) { return { order: [self.baseUrl], known: null }; }
143
+ return self._measure(urls).then(function (order) { return { order: order, known: list }; });
144
+ })
145
+ .then(function (r) {
146
+ self._nodes = r.order;
147
+ self._picking = null;
148
+ // An order is remembered for the hour only when a node list was read; a guess made while nothing answered
149
+ // is tried again by the next client instead of being kept.
150
+ var record = { urls: r.known ? r.order : [], at: r.known ? Date.now() : 0, known: r.known || kept, knownAt: r.known ? Date.now() : (saved && saved.knownAt) || 0 };
151
+ try { storage.setItem(key, JSON.stringify(record)); } catch (e) { /* ignore */ }
152
+ return r.order;
153
+ });
154
+ return this._picking;
155
+ };
156
+
157
+ /** Asks each node in turn for the node list (any node answers the whole list); null when none answers. */
158
+ SMapClient.prototype._discover = function (candidates) {
159
+ var self = this;
160
+ function ask(i) {
161
+ if (i >= candidates.length) { return Promise.resolve(null); }
162
+ return self._timed(candidates[i] + '/v1/nodes', {}, 2000)
163
+ .then(function (res) { return res.ok ? res.json() : null; })
164
+ .then(function (body) {
165
+ var urls = ((body && body.nodes) || []).map(function (n) { return String(n.url).replace(/\/$/, ''); });
166
+ return urls.length ? urls : ask(i + 1);
167
+ }, function () { return ask(i + 1); });
168
+ }
169
+ return ask(0);
170
+ };
171
+
172
+ /** Times one small request to each node: fastest first, silent ones after, baseUrl last when not listed. */
173
+ SMapClient.prototype._measure = function (urls) {
174
+ var self = this;
175
+ return Promise.all(urls.map(function (url) {
176
+ var t0 = Date.now();
177
+ return self._timed(url + '/v1/ping', { cache: 'no-store' }, 1500)
178
+ .then(function (res) { return { url: url, ms: res.ok ? Date.now() - t0 : Infinity }; }, function () { return { url: url, ms: Infinity }; });
179
+ })).then(function (timed) {
180
+ timed.sort(function (a, b) { return a.ms - b.ms; });
181
+ var order = timed.map(function (t) { return t.url; });
182
+ if (order.indexOf(self.baseUrl) < 0) { order.push(self.baseUrl); }
183
+ return order;
184
+ });
185
+ };
186
+
187
+ /** Forgets the chosen node order, so the next request measures again. */
188
+ SMapClient.prototype.resetNodes = function () {
189
+ this._nodes = null;
190
+ var key = 'smap.nodes:' + this.baseUrl;
191
+ try {
192
+ var saved = JSON.parse(store().getItem(key) || 'null');
193
+ // The kept node list survives a reset; only the measured order is forgotten.
194
+ store().setItem(key, JSON.stringify(saved ? { urls: [], at: 0, known: saved.known, knownAt: saved.knownAt } : null));
195
+ } catch (e) { /* ignore */ }
196
+ };
197
+
198
+ /**
199
+ * GET `path` from the nearest node that answers, with the token; a node that fails (network error, 5xx) is moved
200
+ * to the end and the next one is tried. 4xx answers are final.
201
+ *
202
+ * @param {string} path starting with /
203
+ * @param {Object} [headers]
204
+ * @returns {Promise<Response>}
205
+ */
206
+ SMapClient.prototype.request = function (path, headers) {
207
+ var self = this;
208
+ return Promise.all([this.nodes(), this.token()]).then(function (r) {
209
+ var nodes = r[0].slice();
210
+ var token = r[1];
211
+ var last = null;
212
+ function attempt(i) {
213
+ if (i >= nodes.length) { throw last || SMapError('sMap: no node answered', 0, path); }
214
+ var h = {};
215
+ for (var k in (headers || {})) { if (Object.prototype.hasOwnProperty.call(headers, k)) { h[k] = headers[k]; } }
216
+ if (token) { h.Authorization = 'Bearer ' + token; }
217
+ if (self._apiKey) { h['X-SMap-Key'] = self._apiKey; }
218
+ var url = nodes[i] + path;
219
+ return self._timed(url, { headers: h }, self._timeout).then(function (res) {
220
+ if (res.status >= 500) {
221
+ last = SMapError('sMap: HTTP ' + res.status, res.status, url);
222
+ self._demote(nodes[i]);
223
+ return attempt(i + 1);
224
+ }
225
+ if (!res.ok && res.status !== 206) {
226
+ return res.text().then(function (text) { throw SMapError('sMap: ' + (text || ('HTTP ' + res.status)), res.status, url); });
227
+ }
228
+ return res;
229
+ }, function (e) {
230
+ last = SMapError('sMap: ' + (e && e.message ? e.message : 'network error'), 0, url);
231
+ self._demote(nodes[i]);
232
+ return attempt(i + 1);
233
+ });
234
+ }
235
+ return attempt(0);
236
+ });
237
+ };
238
+
239
+ SMapClient.prototype._demote = function (url) {
240
+ if (!this._nodes || this._nodes.length < 2) { return; }
241
+ var i = this._nodes.indexOf(url);
242
+ if (i >= 0) { this._nodes.splice(i, 1); this._nodes.push(url); }
243
+ };
244
+
245
+ function bytes(res) { return res.arrayBuffer().then(function (b) { return new Uint8Array(b); }); }
246
+
247
+ // A map key also goes in the query of styles and TileJSON: the service carries it into their tile URLs, so a
248
+ // renderer given the document reads its tiles with the key.
249
+ SMapClient.prototype._withKey = function (path) {
250
+ if (!this._apiKey) { return path; }
251
+ return path + (path.indexOf('?') >= 0 ? '&' : '?') + 'key=' + encodeURIComponent(this._apiKey);
252
+ };
253
+
254
+ /**
255
+ * A style document ('light', 'white', 'grayscale', 'dark', 'black') with labels in `language`.
256
+ * Its tile URLs point at the node that answered.
257
+ */
258
+ SMapClient.prototype.style = function (flavor, language) {
259
+ var q = language ? '?lang=' + encodeURIComponent(language) : '';
260
+ return this.request(this._withKey('/v1/styles/' + encodeURIComponent(flavor || 'light') + q)).then(function (r) { return r.json(); });
261
+ };
262
+
263
+ /** TileJSON of a source (bounds, zooms, layers, tile URL template). */
264
+ SMapClient.prototype.tileJSON = function (source) {
265
+ return this.request(this._withKey('/v1/tiles/' + encodeURIComponent(source || 'basemap'))).then(function (r) { return r.json(); });
266
+ };
267
+
268
+ /** One vector tile as bytes; null where the source has no tile (204 / 404). */
269
+ SMapClient.prototype.tile = function (source, z, x, y) {
270
+ var path = '/v1/tiles/' + encodeURIComponent(source) + '/' + (z | 0) + '/' + (x | 0) + '/' + (y | 0);
271
+ return this.request(path).then(function (r) { return r.status === 204 ? null : bytes(r); }, function (e) {
272
+ if (e && e.status === 404) { return null; }
273
+ throw e;
274
+ });
275
+ };
276
+
277
+ /** Raw bytes `[start, end]` (inclusive) of a source's archive, where your access allows raw reads. */
278
+ SMapClient.prototype.range = function (source, start, end) {
279
+ return this.request('/' + encodeURIComponent(source) + '.pmtiles', { Range: 'bytes=' + start + '-' + end }).then(bytes);
280
+ };
281
+
282
+ /** A font glyph range (`0-255.pbf` style), for renderers that load fonts themselves. */
283
+ SMapClient.prototype.glyphs = function (fontstack, range) {
284
+ return this.request('/v1/fonts/' + encodeURIComponent(fontstack) + '/' + range + '.pbf').then(bytes);
285
+ };
286
+
287
+ /**
288
+ * Headers and URL rewriting for a renderer that fetches by itself: returns the URL unchanged plus the
289
+ * Authorization (and map key) headers to add for sMap URLs; none for any other host.
290
+ */
291
+ SMapClient.prototype.authorize = function (url) {
292
+ var self = this;
293
+ return this.token().then(function (token) {
294
+ var ours = url.indexOf(self.baseUrl) === 0 || self._seeds.concat(self._nodes || []).some(function (n) { return url.indexOf(n) === 0; });
295
+ var headers = {};
296
+ if (ours && token) { headers.Authorization = 'Bearer ' + token; }
297
+ if (ours && self._apiKey) { headers['X-SMap-Key'] = self._apiKey; }
298
+ return { url: url, headers: headers };
299
+ });
300
+ };
301
+
302
+ return SMapClient;
303
+ }));
304
+
305
+ }).call(typeof globalThis !== 'undefined' ? globalThis : undefined, smapClientModule, smapClientModule.exports, undefined);
306
+ var SMapClient = smapClientModule.exports;
307
+ export { SMapClient };
308
+ export default SMapClient;