@tileflow/react 0.1.0-alpha.19 → 0.1.0-alpha.20
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/GENERATED_OUTPUT_LICENSE.md +21 -0
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +189 -15
- package/TRADEMARKS.md +10 -0
- package/dist/index.d.ts +61 -15
- package/dist/index.js +1116 -212
- package/dist/static.d.ts +3 -2
- package/dist/static.js +74 -123
- package/package.json +17 -8
package/dist/static.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { CSSProperties, ImgHTMLAttributes } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { StaticMapResult } from '@tileflow/static/client';
|
|
4
|
+
import { StaticSceneInput } from '@tileflow/static/scene';
|
|
4
5
|
|
|
5
6
|
type StaticMapBaseProps = StaticSceneInput & {
|
|
6
7
|
alt?: string;
|
|
@@ -25,6 +26,6 @@ type StaticMapCreateProps = {
|
|
|
25
26
|
imageUrl?: never;
|
|
26
27
|
};
|
|
27
28
|
type StaticMapProps = StaticMapBaseProps & (StaticMapCreateProps | StaticMapImageProps);
|
|
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;
|
|
29
|
+
declare function StaticMap({ alt, camera, className, captureId, createUrl, imageStyle, imageUrl, idempotencyKey, keepPreviousImage, loading, map, onError, onImageLoad, onReady, overlays, size, style, theme, }: StaticMapProps): react.JSX.Element;
|
|
29
30
|
|
|
30
31
|
export { StaticMap, type StaticMapProps };
|
package/dist/static.js
CHANGED
|
@@ -8,26 +8,40 @@ import {
|
|
|
8
8
|
useRef,
|
|
9
9
|
useState
|
|
10
10
|
} from "react";
|
|
11
|
-
import { normalizeTileflowCaptureId } from "@tileflow/core";
|
|
11
|
+
import { normalizeTileflowCaptureId } from "@tileflow/core/capture";
|
|
12
12
|
import {
|
|
13
|
+
prepareStaticMapRequest,
|
|
14
|
+
stableStringify as stableStringify2,
|
|
13
15
|
validateStaticMapIdempotencyKey
|
|
14
|
-
} from "@tileflow/static";
|
|
16
|
+
} from "@tileflow/static/client";
|
|
15
17
|
|
|
16
18
|
// src/static-map-request.ts
|
|
17
19
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from "@tileflow/static";
|
|
20
|
+
requestStaticMapUntilReady,
|
|
21
|
+
stableStringify
|
|
22
|
+
} from "@tileflow/static/client";
|
|
21
23
|
var inFlightRequests = /* @__PURE__ */ new Map();
|
|
22
24
|
function createStaticMapRequestKey(input) {
|
|
23
|
-
return stableStringify(
|
|
25
|
+
return stableStringify({
|
|
26
|
+
createUrl: input.createUrl,
|
|
27
|
+
idempotencyKey: input.idempotencyKey,
|
|
28
|
+
sceneKey: input.request.sceneKey
|
|
29
|
+
});
|
|
24
30
|
}
|
|
25
31
|
async function resolveStaticMap(input) {
|
|
26
32
|
throwIfAborted(input.signal);
|
|
27
|
-
|
|
33
|
+
const requestKey = createStaticMapRequestKey(input);
|
|
34
|
+
let entry = inFlightRequests.get(requestKey);
|
|
28
35
|
if (!entry) {
|
|
29
36
|
const controller = new AbortController();
|
|
30
|
-
const promise = requestStaticMapUntilReady(
|
|
37
|
+
const promise = requestStaticMapUntilReady(input.request, {
|
|
38
|
+
createUrl: input.createUrl,
|
|
39
|
+
fetch: input.fetch,
|
|
40
|
+
idempotencyKey: input.idempotencyKey,
|
|
41
|
+
maxWaitMs: input.maxWaitMs,
|
|
42
|
+
pollIntervalMs: 0,
|
|
43
|
+
signal: controller.signal
|
|
44
|
+
});
|
|
31
45
|
const created = {
|
|
32
46
|
controller,
|
|
33
47
|
consumers: 0,
|
|
@@ -36,12 +50,12 @@ async function resolveStaticMap(input) {
|
|
|
36
50
|
};
|
|
37
51
|
const settle = () => {
|
|
38
52
|
created.settled = true;
|
|
39
|
-
if (inFlightRequests.get(
|
|
40
|
-
inFlightRequests.delete(
|
|
53
|
+
if (inFlightRequests.get(requestKey) === created) {
|
|
54
|
+
inFlightRequests.delete(requestKey);
|
|
41
55
|
}
|
|
42
56
|
};
|
|
43
57
|
void promise.then(settle, settle);
|
|
44
|
-
inFlightRequests.set(
|
|
58
|
+
inFlightRequests.set(requestKey, created);
|
|
45
59
|
entry = created;
|
|
46
60
|
}
|
|
47
61
|
entry.consumers += 1;
|
|
@@ -50,97 +64,13 @@ async function resolveStaticMap(input) {
|
|
|
50
64
|
} finally {
|
|
51
65
|
entry.consumers -= 1;
|
|
52
66
|
if (entry.consumers === 0 && !entry.settled) {
|
|
53
|
-
if (inFlightRequests.get(
|
|
54
|
-
inFlightRequests.delete(
|
|
67
|
+
if (inFlightRequests.get(requestKey) === entry) {
|
|
68
|
+
inFlightRequests.delete(requestKey);
|
|
55
69
|
}
|
|
56
70
|
entry.controller.abort();
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
}
|
|
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
74
|
function raceWithAbort(operation, signal) {
|
|
145
75
|
if (!signal) return operation;
|
|
146
76
|
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
@@ -172,15 +102,6 @@ function abortReason(signal) {
|
|
|
172
102
|
error.name = "AbortError";
|
|
173
103
|
return error;
|
|
174
104
|
}
|
|
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
105
|
|
|
185
106
|
// src/static-map.tsx
|
|
186
107
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -201,7 +122,8 @@ function StaticMap({
|
|
|
201
122
|
onReady,
|
|
202
123
|
overlays,
|
|
203
124
|
size,
|
|
204
|
-
style
|
|
125
|
+
style,
|
|
126
|
+
theme
|
|
205
127
|
}) {
|
|
206
128
|
const [result, setResult] = useState(
|
|
207
129
|
imageUrl ? imageResult(imageUrl) : null
|
|
@@ -213,11 +135,17 @@ function StaticMap({
|
|
|
213
135
|
const resolvedCaptureId = normalizeTileflowCaptureId(captureId);
|
|
214
136
|
const onErrorRef = useRef(onError);
|
|
215
137
|
const onReadyRef = useRef(onReady);
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
138
|
+
const sceneInputKey = stableStringify2({ camera, map, overlays, size, theme });
|
|
139
|
+
const preparedSceneCandidate = useMemo(
|
|
140
|
+
() => prepareSceneRequest({ camera, map, overlays, size, theme }),
|
|
141
|
+
[camera, map, overlays, size, theme]
|
|
219
142
|
);
|
|
220
|
-
const
|
|
143
|
+
const preparedScene = useStablePreparedSceneRequest(preparedSceneCandidate);
|
|
144
|
+
const sceneKey = preparedScene.ok ? preparedScene.request.sceneKey : `invalid:${sceneInputKey}`;
|
|
145
|
+
const intentKey = imageUrl ? `image:${imageUrl}:${sceneKey}` : `create:${createUrl}:${idempotencyKey ?? ""}:${sceneKey}`;
|
|
146
|
+
const visibleResult = preparedScene.ok ? result : null;
|
|
147
|
+
const visibleError = preparedScene.ok ? error : preparedScene.error;
|
|
148
|
+
const effectiveCaptureState = preparedScene.ok ? captureState : "error";
|
|
221
149
|
useLayoutEffect(() => {
|
|
222
150
|
imageLoadRunRef.current += 1;
|
|
223
151
|
setCaptureState("loading");
|
|
@@ -244,6 +172,15 @@ function StaticMap({
|
|
|
244
172
|
onReadyRef.current = onReady;
|
|
245
173
|
}, [onError, onReady]);
|
|
246
174
|
useEffect(() => {
|
|
175
|
+
let cancelled = false;
|
|
176
|
+
if (!preparedScene.ok) {
|
|
177
|
+
if (!keepPreviousImage) setResult(null);
|
|
178
|
+
setError(preparedScene.error);
|
|
179
|
+
imageLoadRunRef.current += 1;
|
|
180
|
+
setCaptureState("error");
|
|
181
|
+
onErrorRef.current?.(preparedScene.error);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
247
184
|
if (imageUrl) {
|
|
248
185
|
const nextResult = imageResult(imageUrl);
|
|
249
186
|
setResult(nextResult);
|
|
@@ -251,7 +188,6 @@ function StaticMap({
|
|
|
251
188
|
onReadyRef.current?.(nextResult);
|
|
252
189
|
return;
|
|
253
190
|
}
|
|
254
|
-
let cancelled = false;
|
|
255
191
|
if (!createUrl) {
|
|
256
192
|
return;
|
|
257
193
|
}
|
|
@@ -273,12 +209,7 @@ function StaticMap({
|
|
|
273
209
|
resolveStaticMap({
|
|
274
210
|
createUrl,
|
|
275
211
|
idempotencyKey: validation.key,
|
|
276
|
-
|
|
277
|
-
createUrl,
|
|
278
|
-
idempotencyKey: validation.key,
|
|
279
|
-
sceneKey
|
|
280
|
-
}),
|
|
281
|
-
scene: JSON.parse(sceneKey),
|
|
212
|
+
request: preparedScene.request,
|
|
282
213
|
signal: requestController.signal
|
|
283
214
|
}).then((nextResult) => {
|
|
284
215
|
if (cancelled) {
|
|
@@ -304,15 +235,16 @@ function StaticMap({
|
|
|
304
235
|
cancelled = true;
|
|
305
236
|
requestController.abort();
|
|
306
237
|
};
|
|
307
|
-
}, [createUrl, idempotencyKey, imageUrl,
|
|
238
|
+
}, [createUrl, idempotencyKey, imageUrl, keepPreviousImage, preparedScene]);
|
|
308
239
|
return /* @__PURE__ */ jsx(
|
|
309
240
|
"div",
|
|
310
241
|
{
|
|
311
|
-
"aria-busy": !
|
|
242
|
+
"aria-busy": !visibleResult && !visibleError,
|
|
312
243
|
className,
|
|
313
244
|
"data-tileflow-capture-id": resolvedCaptureId,
|
|
314
245
|
"data-tileflow-map": map,
|
|
315
|
-
"data-tileflow-
|
|
246
|
+
"data-tileflow-theme": preparedScene.ok ? theme : void 0,
|
|
247
|
+
"data-tileflow-state": effectiveCaptureState,
|
|
316
248
|
style: {
|
|
317
249
|
aspectRatio: `${size.width} / ${size.height}`,
|
|
318
250
|
background: "#f4f4f2",
|
|
@@ -320,7 +252,7 @@ function StaticMap({
|
|
|
320
252
|
width: "100%",
|
|
321
253
|
...style
|
|
322
254
|
},
|
|
323
|
-
children:
|
|
255
|
+
children: visibleResult ? /* @__PURE__ */ jsx(
|
|
324
256
|
"img",
|
|
325
257
|
{
|
|
326
258
|
alt,
|
|
@@ -338,7 +270,7 @@ function StaticMap({
|
|
|
338
270
|
void markImageReady(event.currentTarget);
|
|
339
271
|
},
|
|
340
272
|
ref: imageRef,
|
|
341
|
-
src:
|
|
273
|
+
src: visibleResult.imageUrl,
|
|
342
274
|
style: {
|
|
343
275
|
display: "block",
|
|
344
276
|
height: "100%",
|
|
@@ -351,6 +283,25 @@ function StaticMap({
|
|
|
351
283
|
}
|
|
352
284
|
);
|
|
353
285
|
}
|
|
286
|
+
function prepareSceneRequest(scene) {
|
|
287
|
+
try {
|
|
288
|
+
return { ok: true, request: prepareStaticMapRequest(scene) };
|
|
289
|
+
} catch (error) {
|
|
290
|
+
return {
|
|
291
|
+
error: error instanceof Error ? error : new Error("Invalid Tileflow static scene"),
|
|
292
|
+
ok: false
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function useStablePreparedSceneRequest(value) {
|
|
297
|
+
const valueRef = useRef(value);
|
|
298
|
+
const current = valueRef.current;
|
|
299
|
+
const equivalent = current.ok === value.ok && (current.ok && value.ok ? current.request.sceneKey === value.request.sceneKey : !current.ok && !value.ok && current.error.message === value.error.message);
|
|
300
|
+
if (!equivalent) {
|
|
301
|
+
valueRef.current = value;
|
|
302
|
+
}
|
|
303
|
+
return valueRef.current;
|
|
304
|
+
}
|
|
354
305
|
function imageResult(imageUrl) {
|
|
355
306
|
return {
|
|
356
307
|
cached: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tileflow/react",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20",
|
|
4
|
+
"license": "Apache-2.0",
|
|
4
5
|
"description": "React components for rendering Tileflow maps with MapLibre.",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"sideEffects": false,
|
|
@@ -23,7 +24,11 @@
|
|
|
23
24
|
"main": "./dist/index.js",
|
|
24
25
|
"types": "./dist/index.d.ts",
|
|
25
26
|
"files": [
|
|
26
|
-
"dist"
|
|
27
|
+
"dist",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"NOTICE",
|
|
30
|
+
"GENERATED_OUTPUT_LICENSE.md",
|
|
31
|
+
"TRADEMARKS.md"
|
|
27
32
|
],
|
|
28
33
|
"exports": {
|
|
29
34
|
".": {
|
|
@@ -41,22 +46,26 @@
|
|
|
41
46
|
"access": "public"
|
|
42
47
|
},
|
|
43
48
|
"dependencies": {
|
|
44
|
-
"@tileflow/core": ">=0.1.0-alpha.
|
|
45
|
-
"@tileflow/
|
|
49
|
+
"@tileflow/core": ">=0.1.0-alpha.21 <0.1.0-beta.0",
|
|
50
|
+
"@tileflow/interactions": ">=0.1.0-alpha.0 <0.1.0-beta.0",
|
|
51
|
+
"@tileflow/static": ">=0.1.0-alpha.18 <0.1.0-beta.0"
|
|
46
52
|
},
|
|
47
53
|
"peerDependencies": {
|
|
48
|
-
"maplibre-gl": ">=5",
|
|
49
|
-
"react": ">=18"
|
|
54
|
+
"maplibre-gl": ">=5 <7",
|
|
55
|
+
"react": ">=18 <20",
|
|
56
|
+
"react-dom": ">=18 <20"
|
|
50
57
|
},
|
|
51
58
|
"devDependencies": {
|
|
59
|
+
"@types/react": "^19.2.17",
|
|
60
|
+
"@types/react-dom": "^19.2.3",
|
|
52
61
|
"maplibre-gl": "^5.24.0",
|
|
53
62
|
"react": "^19.2.7",
|
|
54
|
-
"
|
|
63
|
+
"react-dom": "^19.2.7"
|
|
55
64
|
},
|
|
56
65
|
"scripts": {
|
|
57
66
|
"build": "tsup",
|
|
58
67
|
"verify": "pnpm run build && tsx --test test/*.test.ts",
|
|
59
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
68
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.contract.json",
|
|
60
69
|
"lint": "tsc -p tsconfig.json --noEmit"
|
|
61
70
|
}
|
|
62
71
|
}
|