@tileflow/react 0.1.0-alpha.16 → 0.1.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/dist/index.js +84 -96
- package/dist/static.d.ts +3 -1
- package/dist/static.js +202 -43
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ manifest:
|
|
|
38
38
|
Or bypass the manifest and load a hosted style directly:
|
|
39
39
|
|
|
40
40
|
```tsx
|
|
41
|
-
<Map styleUrl="https://api.tileflow.dev/
|
|
41
|
+
<Map styleUrl="https://api.tileflow.dev/maps/map_1234567890abcdef/style.json" />
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
For simple hosted static images, use image mode:
|
|
@@ -55,6 +55,35 @@ the production image behavior locally.
|
|
|
55
55
|
For full static scenes with bounds, overlays, and server-side precache, use
|
|
56
56
|
`StaticMap` from `@tileflow/react/static` or helpers from `@tileflow/static`.
|
|
57
57
|
|
|
58
|
+
```tsx
|
|
59
|
+
import {StaticMap} from '@tileflow/react/static';
|
|
60
|
+
import {useState} from 'react';
|
|
61
|
+
import {createStaticMapIdempotencyKey} from '@tileflow/static';
|
|
62
|
+
|
|
63
|
+
export function Preview() {
|
|
64
|
+
const [idempotencyKey] = useState(createStaticMapIdempotencyKey);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<StaticMap
|
|
68
|
+
map="madrid"
|
|
69
|
+
camera={{type: 'center', center: [-3.7038, 40.4168], zoom: 12}}
|
|
70
|
+
size={{width: 1200, height: 800}}
|
|
71
|
+
createUrl="/api/static-maps"
|
|
72
|
+
idempotencyKey={idempotencyKey}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`idempotencyKey` is required in create mode. Keep one key for one intentional create action and
|
|
79
|
+
reuse it across retries and remounts. Concurrent React consumers share work only when URL,
|
|
80
|
+
normalized scene, and idempotency key all identify the same operation; unmounting the final
|
|
81
|
+
consumer aborts its in-flight request.
|
|
82
|
+
|
|
83
|
+
Hosted interactive maps automatically preflight a short-lived commercial session grant before
|
|
84
|
+
eligible resources. Setting `analytics={{enabled: false}}` disables the optional beacon only; it
|
|
85
|
+
does not remove hosted authorization or override a user `mapOptions.transformRequest` callback.
|
|
86
|
+
|
|
58
87
|
## Headless capture readiness
|
|
59
88
|
|
|
60
89
|
Use `captureId` to disambiguate multiple maps with the same configured name:
|
package/dist/index.js
CHANGED
|
@@ -10,20 +10,24 @@ import {
|
|
|
10
10
|
} from "react";
|
|
11
11
|
import maplibregl from "maplibre-gl";
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
createTileflowSessionController,
|
|
14
14
|
defaultTileflowManifestUrl,
|
|
15
15
|
loadTileflowManifest,
|
|
16
16
|
mergeTileflowAnalytics,
|
|
17
17
|
normalizeTileflowCaptureId,
|
|
18
18
|
normalizeTileflowStaticImageSize,
|
|
19
|
-
resolveTileflowAnalyticsRequestUrl,
|
|
20
19
|
resolveTileflowManifestMap,
|
|
21
20
|
resolveTileflowMapMode,
|
|
22
21
|
resolveTileflowRuntimeStyle,
|
|
23
22
|
resolveTileflowStaticImageUrl,
|
|
24
|
-
shouldLoadTileflowManifest
|
|
25
|
-
startTileflowSession
|
|
23
|
+
shouldLoadTileflowManifest
|
|
26
24
|
} from "@tileflow/core";
|
|
25
|
+
import {
|
|
26
|
+
attachTileflowMapLifecycle,
|
|
27
|
+
createTileflowMarkerController,
|
|
28
|
+
createTileflowSessionStarter,
|
|
29
|
+
createTileflowTransformRequest
|
|
30
|
+
} from "@tileflow/core/browser";
|
|
27
31
|
import { jsx } from "react/jsx-runtime";
|
|
28
32
|
var defaultCenter = [0, 20];
|
|
29
33
|
var defaultMarkers = [];
|
|
@@ -55,7 +59,6 @@ function Map({
|
|
|
55
59
|
const containerRef = useRef(null);
|
|
56
60
|
const imageRef = useRef(null);
|
|
57
61
|
const mapRef = useRef(null);
|
|
58
|
-
const markerInstancesRef = useRef([]);
|
|
59
62
|
const stableMapOptions = useStableMapOptions(mapOptions);
|
|
60
63
|
const resolvedCenter = useStableMapOptionValue(
|
|
61
64
|
center ?? stableMapOptions?.center ?? defaultCenter
|
|
@@ -68,9 +71,24 @@ function Map({
|
|
|
68
71
|
const onLoadRef = useRef(onLoad);
|
|
69
72
|
const mapNameRef = useRef(mapName);
|
|
70
73
|
const resolvedAnalyticsRef = useRef(void 0);
|
|
71
|
-
const sessionStartsRef = useRef(/* @__PURE__ */ new Set());
|
|
72
74
|
const readinessRunRef = useRef(0);
|
|
73
|
-
const
|
|
75
|
+
const markerController = useMemo(
|
|
76
|
+
() => createTileflowMarkerController({
|
|
77
|
+
attach(markerInstance, map, marker) {
|
|
78
|
+
markerInstance.setLngLat(marker.coordinates).addTo(map);
|
|
79
|
+
markerInstance.getElement().title = marker.label ?? marker.id;
|
|
80
|
+
},
|
|
81
|
+
create(marker) {
|
|
82
|
+
return new maplibregl.Marker({
|
|
83
|
+
color: marker.color ?? "#C6A15B"
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
remove(markerInstance) {
|
|
87
|
+
markerInstance.remove();
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
[]
|
|
91
|
+
);
|
|
74
92
|
const [manifestMap, setManifestMap] = useState(null);
|
|
75
93
|
const [imageSize, setImageSize] = useState(null);
|
|
76
94
|
const [captureState, setCaptureState] = useState("loading");
|
|
@@ -165,14 +183,6 @@ function Map({
|
|
|
165
183
|
}) : void 0),
|
|
166
184
|
[imageSize, imageUrl, isImageMode, manifestMap, resolvedZoom, staticImageCenter]
|
|
167
185
|
);
|
|
168
|
-
const transformRequest = useMemo(
|
|
169
|
-
() => createTransformRequest(
|
|
170
|
-
() => resolvedAnalyticsRef.current,
|
|
171
|
-
sessionId,
|
|
172
|
-
stableMapOptions?.transformRequest
|
|
173
|
-
),
|
|
174
|
-
[sessionId, stableMapOptions?.transformRequest]
|
|
175
|
-
);
|
|
176
186
|
useEffect(() => {
|
|
177
187
|
if (!isImageMode || imageUrl || !containerRef.current) {
|
|
178
188
|
setImageSize(null);
|
|
@@ -199,25 +209,22 @@ function Map({
|
|
|
199
209
|
if (!containerRef.current || !runtimeStyle) {
|
|
200
210
|
return;
|
|
201
211
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
};
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
});
|
|
219
|
-
};
|
|
220
|
-
setLoading();
|
|
212
|
+
readinessRunRef.current += 1;
|
|
213
|
+
setCaptureState("loading");
|
|
214
|
+
const session = createTileflowSessionController({ source: "react" });
|
|
215
|
+
const sessionStarter = createTileflowSessionStarter({
|
|
216
|
+
getSessionId: () => session.sessionId,
|
|
217
|
+
sessionId: session.sessionId,
|
|
218
|
+
source: "react"
|
|
219
|
+
});
|
|
220
|
+
const transformRequest = createTileflowTransformRequest({
|
|
221
|
+
always: true,
|
|
222
|
+
asyncAnalyticsTiming: "resolution",
|
|
223
|
+
getAnalytics: () => resolvedAnalyticsRef.current,
|
|
224
|
+
sessionController: session,
|
|
225
|
+
sessionId: session.sessionId,
|
|
226
|
+
transformRequest: stableMapOptions?.transformRequest ?? void 0
|
|
227
|
+
});
|
|
221
228
|
const map = new maplibregl.Map({
|
|
222
229
|
...stableMapOptions,
|
|
223
230
|
container: containerRef.current,
|
|
@@ -237,36 +244,50 @@ function Map({
|
|
|
237
244
|
map.resize();
|
|
238
245
|
});
|
|
239
246
|
resizeObserver.observe(containerRef.current);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
247
|
+
const lifecycle = attachTileflowMapLifecycle({
|
|
248
|
+
getSession: () => {
|
|
249
|
+
const analyticsForLoad = resolvedAnalyticsRef.current;
|
|
250
|
+
return {
|
|
251
|
+
analytics: analyticsForLoad,
|
|
252
|
+
styleId: analyticsForLoad?.styleId ?? (typeof runtimeStyle.style === "string" ? runtimeStyle.style : mapNameRef.current)
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
map,
|
|
256
|
+
onLoad: (loadedMap) => {
|
|
257
|
+
onLoadRef.current?.(loadedMap);
|
|
258
|
+
},
|
|
259
|
+
scheduler: {
|
|
260
|
+
cancelFrame: (frame) => cancelAnimationFrame(frame),
|
|
261
|
+
requestFrame: (callback) => requestAnimationFrame(callback)
|
|
262
|
+
},
|
|
263
|
+
sessionStarter,
|
|
264
|
+
setState: setCaptureState,
|
|
265
|
+
subscribe: (subscribedMap, event, listener) => {
|
|
266
|
+
const subscription = subscribedMap.on(event, listener);
|
|
267
|
+
return () => subscription.unsubscribe();
|
|
255
268
|
}
|
|
256
269
|
});
|
|
257
|
-
map.on("dataloading", setLoading);
|
|
258
|
-
map.on("styledataloading", setLoading);
|
|
259
|
-
map.on("idle", setIdleAfterFrames);
|
|
260
|
-
map.on("error", setError);
|
|
261
270
|
return () => {
|
|
262
271
|
readinessRunRef.current += 1;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
272
|
+
try {
|
|
273
|
+
lifecycle.dispose();
|
|
274
|
+
} finally {
|
|
275
|
+
try {
|
|
276
|
+
resizeObserver.disconnect();
|
|
277
|
+
} finally {
|
|
278
|
+
try {
|
|
279
|
+
markerController.clear();
|
|
280
|
+
} finally {
|
|
281
|
+
try {
|
|
282
|
+
map.remove();
|
|
283
|
+
} finally {
|
|
284
|
+
if (mapRef.current === map) mapRef.current = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
268
289
|
};
|
|
269
|
-
}, [resolvedInteractive, runtimeStyle,
|
|
290
|
+
}, [markerController, resolvedInteractive, runtimeStyle, stableMapOptions]);
|
|
270
291
|
useLayoutEffect(() => {
|
|
271
292
|
if (!isImageMode) return;
|
|
272
293
|
readinessRunRef.current += 1;
|
|
@@ -291,23 +312,15 @@ function Map({
|
|
|
291
312
|
}
|
|
292
313
|
useEffect(() => {
|
|
293
314
|
const map = mapRef.current;
|
|
294
|
-
|
|
295
|
-
markerInstancesRef.current = [];
|
|
315
|
+
markerController.clear();
|
|
296
316
|
if (!map) {
|
|
297
317
|
return;
|
|
298
318
|
}
|
|
299
|
-
|
|
300
|
-
const markerInstance = new maplibregl.Marker({
|
|
301
|
-
color: marker.color ?? "#C6A15B"
|
|
302
|
-
}).setLngLat(marker.coordinates).addTo(map);
|
|
303
|
-
markerInstance.getElement().title = marker.label ?? marker.id;
|
|
304
|
-
return markerInstance;
|
|
305
|
-
});
|
|
319
|
+
markerController.replace(map, markers);
|
|
306
320
|
return () => {
|
|
307
|
-
|
|
308
|
-
markerInstancesRef.current = [];
|
|
321
|
+
markerController.clear();
|
|
309
322
|
};
|
|
310
|
-
}, [markers, runtimeStyle]);
|
|
323
|
+
}, [markerController, markers, runtimeStyle]);
|
|
311
324
|
const frameStyle = {
|
|
312
325
|
height,
|
|
313
326
|
minHeight: 240,
|
|
@@ -361,31 +374,6 @@ function Map({
|
|
|
361
374
|
}
|
|
362
375
|
);
|
|
363
376
|
}
|
|
364
|
-
function createTransformRequest(getAnalytics, sessionId, userTransformRequest) {
|
|
365
|
-
return (url, resourceType) => {
|
|
366
|
-
const request = userTransformRequest?.(url, resourceType);
|
|
367
|
-
if (isPromiseLike(request)) {
|
|
368
|
-
return request.then(
|
|
369
|
-
(resolvedRequest) => applyTileflowAnalyticsRequest(url, resolvedRequest, getAnalytics(), sessionId) ?? resolvedRequest
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
return applyTileflowAnalyticsRequest(url, request, getAnalytics(), sessionId);
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
function applyTileflowAnalyticsRequest(url, request, analytics, sessionId) {
|
|
376
|
-
const requestUrl = request?.url ?? url;
|
|
377
|
-
const nextUrl = resolveTileflowAnalyticsRequestUrl(requestUrl, analytics, sessionId);
|
|
378
|
-
if (!nextUrl) {
|
|
379
|
-
return request;
|
|
380
|
-
}
|
|
381
|
-
return request ? { ...request, url: nextUrl } : { url: nextUrl };
|
|
382
|
-
}
|
|
383
|
-
function isPromiseLike(value) {
|
|
384
|
-
return Boolean(value && typeof value.then === "function");
|
|
385
|
-
}
|
|
386
|
-
function clearMarkers(markerInstances) {
|
|
387
|
-
markerInstances.forEach((marker) => marker.remove());
|
|
388
|
-
}
|
|
389
377
|
function useStableMapOptions(options) {
|
|
390
378
|
return useStableMapOptionValue(options);
|
|
391
379
|
}
|
package/dist/static.d.ts
CHANGED
|
@@ -16,13 +16,15 @@ type StaticMapBaseProps = StaticSceneInput & {
|
|
|
16
16
|
};
|
|
17
17
|
type StaticMapImageProps = {
|
|
18
18
|
createUrl?: never;
|
|
19
|
+
idempotencyKey?: never;
|
|
19
20
|
imageUrl: string;
|
|
20
21
|
};
|
|
21
22
|
type StaticMapCreateProps = {
|
|
22
23
|
createUrl: string;
|
|
24
|
+
idempotencyKey: string;
|
|
23
25
|
imageUrl?: never;
|
|
24
26
|
};
|
|
25
27
|
type StaticMapProps = StaticMapBaseProps & (StaticMapCreateProps | StaticMapImageProps);
|
|
26
|
-
declare function StaticMap({ alt, camera, className, captureId, createUrl, imageStyle, imageUrl, keepPreviousImage, loading, map, onError, onImageLoad, onReady, overlays, size, style, }: StaticMapProps): react.JSX.Element;
|
|
28
|
+
declare function StaticMap({ alt, camera, className, captureId, createUrl, imageStyle, imageUrl, idempotencyKey, keepPreviousImage, loading, map, onError, onImageLoad, onReady, overlays, size, style, }: StaticMapProps): react.JSX.Element;
|
|
27
29
|
|
|
28
30
|
export { StaticMap, type StaticMapProps };
|
package/dist/static.js
CHANGED
|
@@ -9,8 +9,181 @@ import {
|
|
|
9
9
|
useState
|
|
10
10
|
} from "react";
|
|
11
11
|
import { normalizeTileflowCaptureId } from "@tileflow/core";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
validateStaticMapIdempotencyKey
|
|
14
|
+
} from "@tileflow/static";
|
|
15
|
+
|
|
16
|
+
// src/static-map-request.ts
|
|
17
|
+
import {
|
|
18
|
+
staticMapProcessingResultSchema,
|
|
19
|
+
staticMapReadyResultSchema
|
|
20
|
+
} from "@tileflow/static";
|
|
13
21
|
var inFlightRequests = /* @__PURE__ */ new Map();
|
|
22
|
+
function createStaticMapRequestKey(input) {
|
|
23
|
+
return stableStringify(input);
|
|
24
|
+
}
|
|
25
|
+
async function resolveStaticMap(input) {
|
|
26
|
+
throwIfAborted(input.signal);
|
|
27
|
+
let entry = inFlightRequests.get(input.requestKey);
|
|
28
|
+
if (!entry) {
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const promise = requestStaticMapUntilReady({ ...input, signal: controller.signal });
|
|
31
|
+
const created = {
|
|
32
|
+
controller,
|
|
33
|
+
consumers: 0,
|
|
34
|
+
promise,
|
|
35
|
+
settled: false
|
|
36
|
+
};
|
|
37
|
+
const settle = () => {
|
|
38
|
+
created.settled = true;
|
|
39
|
+
if (inFlightRequests.get(input.requestKey) === created) {
|
|
40
|
+
inFlightRequests.delete(input.requestKey);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
void promise.then(settle, settle);
|
|
44
|
+
inFlightRequests.set(input.requestKey, created);
|
|
45
|
+
entry = created;
|
|
46
|
+
}
|
|
47
|
+
entry.consumers += 1;
|
|
48
|
+
try {
|
|
49
|
+
return await raceWithAbort(entry.promise, input.signal);
|
|
50
|
+
} finally {
|
|
51
|
+
entry.consumers -= 1;
|
|
52
|
+
if (entry.consumers === 0 && !entry.settled) {
|
|
53
|
+
if (inFlightRequests.get(input.requestKey) === entry) {
|
|
54
|
+
inFlightRequests.delete(input.requestKey);
|
|
55
|
+
}
|
|
56
|
+
entry.controller.abort();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function requestStaticMapUntilReady(input) {
|
|
61
|
+
const maxWaitMs = input.maxWaitMs ?? 3e4;
|
|
62
|
+
if (!Number.isSafeInteger(maxWaitMs) || maxWaitMs < 1 || maxWaitMs > 12e4) {
|
|
63
|
+
throw new Error("Static map maxWaitMs must be an integer from 1 to 120000");
|
|
64
|
+
}
|
|
65
|
+
return runWithinBudget(maxWaitMs, input.signal, async (signal) => {
|
|
66
|
+
const body = JSON.stringify(input.scene);
|
|
67
|
+
const fetcher = input.fetch ?? fetch;
|
|
68
|
+
let operationId = null;
|
|
69
|
+
while (true) {
|
|
70
|
+
const response = await fetcher(input.createUrl, {
|
|
71
|
+
body,
|
|
72
|
+
headers: {
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"Idempotency-Key": input.idempotencyKey
|
|
75
|
+
},
|
|
76
|
+
method: "POST",
|
|
77
|
+
signal
|
|
78
|
+
});
|
|
79
|
+
throwIfAborted(signal);
|
|
80
|
+
if (!response.ok) throw new Error(`Static map endpoint failed: ${response.status}`);
|
|
81
|
+
let value;
|
|
82
|
+
try {
|
|
83
|
+
value = await response.json();
|
|
84
|
+
} catch {
|
|
85
|
+
throw new Error("Static map endpoint returned invalid JSON");
|
|
86
|
+
}
|
|
87
|
+
throwIfAborted(signal);
|
|
88
|
+
if (response.status !== 202) {
|
|
89
|
+
const ready = staticMapReadyResultSchema.safeParse(value);
|
|
90
|
+
if (!ready.success) throw new Error("Static map endpoint returned an invalid response");
|
|
91
|
+
assertStaticMapOperationIdentity(operationId, ready.data.operationId);
|
|
92
|
+
return ready.data;
|
|
93
|
+
}
|
|
94
|
+
const pending = staticMapProcessingResultSchema.safeParse(value);
|
|
95
|
+
if (!pending.success) throw new Error("Static map endpoint returned an invalid response");
|
|
96
|
+
assertStaticMapOperationIdentity(operationId, pending.data.operationId);
|
|
97
|
+
operationId ??= pending.data.operationId;
|
|
98
|
+
await delay(pending.data.retryAfterMs, signal);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function stableStringify(value) {
|
|
103
|
+
return JSON.stringify(sortJson(value));
|
|
104
|
+
}
|
|
105
|
+
function assertStaticMapOperationIdentity(expected, actual) {
|
|
106
|
+
if (expected !== null && actual !== expected) {
|
|
107
|
+
throw new Error("Static map endpoint changed operation identity while polling");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function runWithinBudget(maxWaitMs, externalSignal, operation) {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const timeoutError = new Error(`Static map request timed out after ${maxWaitMs}ms`);
|
|
113
|
+
const abortFromConsumer = () => controller.abort(externalSignal?.reason);
|
|
114
|
+
if (externalSignal?.aborted) abortFromConsumer();
|
|
115
|
+
else externalSignal?.addEventListener("abort", abortFromConsumer, { once: true });
|
|
116
|
+
const timeout = setTimeout(() => controller.abort(timeoutError), maxWaitMs);
|
|
117
|
+
try {
|
|
118
|
+
throwIfAborted(controller.signal);
|
|
119
|
+
const pending = operation(controller.signal);
|
|
120
|
+
return await raceWithAbort(pending, controller.signal);
|
|
121
|
+
} finally {
|
|
122
|
+
clearTimeout(timeout);
|
|
123
|
+
externalSignal?.removeEventListener("abort", abortFromConsumer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function delay(milliseconds, signal) {
|
|
127
|
+
if (milliseconds === 0) {
|
|
128
|
+
throwIfAborted(signal);
|
|
129
|
+
return Promise.resolve();
|
|
130
|
+
}
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const timeout = setTimeout(() => {
|
|
133
|
+
signal.removeEventListener("abort", onAbort);
|
|
134
|
+
resolve();
|
|
135
|
+
}, milliseconds);
|
|
136
|
+
const onAbort = () => {
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
reject(abortReason(signal));
|
|
139
|
+
};
|
|
140
|
+
if (signal.aborted) onAbort();
|
|
141
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function raceWithAbort(operation, signal) {
|
|
145
|
+
if (!signal) return operation;
|
|
146
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const onAbort = () => {
|
|
149
|
+
cleanup();
|
|
150
|
+
reject(abortReason(signal));
|
|
151
|
+
};
|
|
152
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
153
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
154
|
+
operation.then(
|
|
155
|
+
(value) => {
|
|
156
|
+
cleanup();
|
|
157
|
+
resolve(value);
|
|
158
|
+
},
|
|
159
|
+
(error) => {
|
|
160
|
+
cleanup();
|
|
161
|
+
reject(error);
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function throwIfAborted(signal) {
|
|
167
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
168
|
+
}
|
|
169
|
+
function abortReason(signal) {
|
|
170
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
171
|
+
const error = new Error("Static map request was aborted");
|
|
172
|
+
error.name = "AbortError";
|
|
173
|
+
return error;
|
|
174
|
+
}
|
|
175
|
+
function sortJson(value) {
|
|
176
|
+
if (Array.isArray(value)) return value.map(sortJson);
|
|
177
|
+
if (value && typeof value === "object") {
|
|
178
|
+
return Object.fromEntries(
|
|
179
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, sortJson(entry)])
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/static-map.tsx
|
|
186
|
+
import { jsx } from "react/jsx-runtime";
|
|
14
187
|
function StaticMap({
|
|
15
188
|
alt = "",
|
|
16
189
|
camera,
|
|
@@ -19,6 +192,7 @@ function StaticMap({
|
|
|
19
192
|
createUrl,
|
|
20
193
|
imageStyle,
|
|
21
194
|
imageUrl,
|
|
195
|
+
idempotencyKey,
|
|
22
196
|
keepPreviousImage = false,
|
|
23
197
|
loading = "lazy",
|
|
24
198
|
map,
|
|
@@ -43,7 +217,7 @@ function StaticMap({
|
|
|
43
217
|
() => stableStringify({ camera, map, overlays, size }),
|
|
44
218
|
[camera, map, overlays, size]
|
|
45
219
|
);
|
|
46
|
-
const
|
|
220
|
+
const intentKey = imageUrl ? `image:${imageUrl}` : `create:${createUrl}:${idempotencyKey ?? ""}:${sceneKey}`;
|
|
47
221
|
useLayoutEffect(() => {
|
|
48
222
|
imageLoadRunRef.current += 1;
|
|
49
223
|
setCaptureState("loading");
|
|
@@ -52,7 +226,7 @@ function StaticMap({
|
|
|
52
226
|
if (image.naturalWidth === 0) setCaptureState("error");
|
|
53
227
|
else void markImageReady(image);
|
|
54
228
|
}
|
|
55
|
-
}, [
|
|
229
|
+
}, [intentKey]);
|
|
56
230
|
async function markImageReady(image) {
|
|
57
231
|
const runId = ++imageLoadRunRef.current;
|
|
58
232
|
try {
|
|
@@ -81,14 +255,31 @@ function StaticMap({
|
|
|
81
255
|
if (!createUrl) {
|
|
82
256
|
return;
|
|
83
257
|
}
|
|
258
|
+
const validation = validateStaticMapIdempotencyKey(idempotencyKey ?? "");
|
|
259
|
+
if (!validation.ok) {
|
|
260
|
+
const keyError = new Error(`Invalid Static Maps idempotency key: ${validation.error}`);
|
|
261
|
+
if (!keepPreviousImage) setResult(null);
|
|
262
|
+
setError(keyError);
|
|
263
|
+
imageLoadRunRef.current += 1;
|
|
264
|
+
setCaptureState("error");
|
|
265
|
+
onErrorRef.current?.(keyError);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
84
268
|
if (!keepPreviousImage) {
|
|
85
269
|
setResult(null);
|
|
86
270
|
}
|
|
87
271
|
setError(null);
|
|
272
|
+
const requestController = new AbortController();
|
|
88
273
|
resolveStaticMap({
|
|
89
274
|
createUrl,
|
|
90
|
-
|
|
91
|
-
|
|
275
|
+
idempotencyKey: validation.key,
|
|
276
|
+
requestKey: createStaticMapRequestKey({
|
|
277
|
+
createUrl,
|
|
278
|
+
idempotencyKey: validation.key,
|
|
279
|
+
sceneKey
|
|
280
|
+
}),
|
|
281
|
+
scene: JSON.parse(sceneKey),
|
|
282
|
+
signal: requestController.signal
|
|
92
283
|
}).then((nextResult) => {
|
|
93
284
|
if (cancelled) {
|
|
94
285
|
return;
|
|
@@ -111,8 +302,9 @@ function StaticMap({
|
|
|
111
302
|
});
|
|
112
303
|
return () => {
|
|
113
304
|
cancelled = true;
|
|
305
|
+
requestController.abort();
|
|
114
306
|
};
|
|
115
|
-
}, [createUrl, imageUrl,
|
|
307
|
+
}, [createUrl, idempotencyKey, imageUrl, intentKey, keepPreviousImage, sceneKey]);
|
|
116
308
|
return /* @__PURE__ */ jsx(
|
|
117
309
|
"div",
|
|
118
310
|
{
|
|
@@ -159,50 +351,17 @@ function StaticMap({
|
|
|
159
351
|
}
|
|
160
352
|
);
|
|
161
353
|
}
|
|
162
|
-
async function resolveStaticMap(input) {
|
|
163
|
-
const existing = inFlightRequests.get(input.requestKey);
|
|
164
|
-
if (existing) {
|
|
165
|
-
return existing;
|
|
166
|
-
}
|
|
167
|
-
const promise = fetch(input.createUrl, {
|
|
168
|
-
body: JSON.stringify(input.scene),
|
|
169
|
-
headers: {
|
|
170
|
-
"Content-Type": "application/json"
|
|
171
|
-
},
|
|
172
|
-
method: "POST"
|
|
173
|
-
}).then(async (response) => {
|
|
174
|
-
if (!response.ok) {
|
|
175
|
-
throw new Error(`Static map endpoint failed: ${response.status}`);
|
|
176
|
-
}
|
|
177
|
-
return await response.json();
|
|
178
|
-
}).finally(() => {
|
|
179
|
-
inFlightRequests.delete(input.requestKey);
|
|
180
|
-
});
|
|
181
|
-
inFlightRequests.set(input.requestKey, promise);
|
|
182
|
-
return promise;
|
|
183
|
-
}
|
|
184
354
|
function imageResult(imageUrl) {
|
|
185
355
|
return {
|
|
186
356
|
cached: true,
|
|
187
357
|
hash: "",
|
|
188
358
|
imageUrl,
|
|
189
|
-
|
|
359
|
+
operationId: null,
|
|
360
|
+
remainingUnits: null,
|
|
361
|
+
status: "ready",
|
|
362
|
+
unitCost: 0
|
|
190
363
|
};
|
|
191
364
|
}
|
|
192
|
-
function stableStringify(value) {
|
|
193
|
-
return JSON.stringify(sortJson(value));
|
|
194
|
-
}
|
|
195
|
-
function sortJson(value) {
|
|
196
|
-
if (Array.isArray(value)) {
|
|
197
|
-
return value.map(sortJson);
|
|
198
|
-
}
|
|
199
|
-
if (value && typeof value === "object") {
|
|
200
|
-
return Object.fromEntries(
|
|
201
|
-
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, sortJson(entry)])
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
return value;
|
|
205
|
-
}
|
|
206
365
|
export {
|
|
207
366
|
StaticMap
|
|
208
367
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tileflow/react",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.18",
|
|
4
4
|
"description": "React components for rendering Tileflow maps with MapLibre.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@tileflow/core": "0.1.0-alpha.
|
|
45
|
-
"@tileflow/static": "0.1.0-alpha.
|
|
44
|
+
"@tileflow/core": ">=0.1.0-alpha.18 <0.1.0-beta.0",
|
|
45
|
+
"@tileflow/static": ">=0.1.0-alpha.17 <0.1.0-beta.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"maplibre-gl": ">=5",
|