@tileflow/react 0.1.0-alpha.3 → 0.1.0-alpha.30
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/LICENSE +201 -0
- package/README.md +273 -7
- package/dist/index.d.ts +75 -33
- package/dist/index.js +1308 -354
- package/dist/static.d.ts +8 -4
- package/dist/static.js +202 -49
- package/package.json +21 -16
package/dist/static.d.ts
CHANGED
|
@@ -1,27 +1,31 @@
|
|
|
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;
|
|
7
8
|
className?: string;
|
|
9
|
+
captureId?: string;
|
|
8
10
|
imageStyle?: CSSProperties;
|
|
9
11
|
keepPreviousImage?: boolean;
|
|
10
|
-
loading?: ImgHTMLAttributes<HTMLImageElement>[
|
|
12
|
+
loading?: ImgHTMLAttributes<HTMLImageElement>['loading'];
|
|
11
13
|
onError?: (error: Error) => void;
|
|
12
|
-
onImageLoad?: ImgHTMLAttributes<HTMLImageElement>[
|
|
14
|
+
onImageLoad?: ImgHTMLAttributes<HTMLImageElement>['onLoad'];
|
|
13
15
|
onReady?: (result: StaticMapResult) => void;
|
|
14
16
|
style?: CSSProperties;
|
|
15
17
|
};
|
|
16
18
|
type StaticMapImageProps = {
|
|
17
19
|
createUrl?: never;
|
|
20
|
+
idempotencyKey?: never;
|
|
18
21
|
imageUrl: string;
|
|
19
22
|
};
|
|
20
23
|
type StaticMapCreateProps = {
|
|
21
24
|
createUrl: string;
|
|
25
|
+
idempotencyKey: string;
|
|
22
26
|
imageUrl?: never;
|
|
23
27
|
};
|
|
24
28
|
type StaticMapProps = StaticMapBaseProps & (StaticMapCreateProps | StaticMapImageProps);
|
|
25
|
-
declare function StaticMap({ alt, camera, className, createUrl, imageStyle, imageUrl, keepPreviousImage, loading, map, onError, onImageLoad, onReady, overlays, size, style, }: StaticMapProps): react.JSX.Element;
|
|
29
|
+
declare function StaticMap({ alt, attribution, camera, className, captureId, createUrl, format, imageStyle, imageUrl, idempotencyKey, keepPreviousImage, loading, map, onError, onImageLoad, onReady, overlays, size, style, theme, }: StaticMapProps): react.JSX.Element;
|
|
26
30
|
|
|
27
31
|
export { StaticMap, type StaticMapProps };
|
package/dist/static.js
CHANGED
|
@@ -1,19 +1,121 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
// src/static-map.tsx
|
|
2
4
|
import {
|
|
3
5
|
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
4
7
|
useMemo,
|
|
5
8
|
useRef,
|
|
6
9
|
useState
|
|
7
10
|
} from "react";
|
|
8
|
-
import {
|
|
11
|
+
import { normalizeTileflowCaptureId } from "@tileflow/core/capture";
|
|
12
|
+
import {
|
|
13
|
+
prepareStaticMapRequest,
|
|
14
|
+
stableStringify as stableStringify2,
|
|
15
|
+
validateStaticMapIdempotencyKey
|
|
16
|
+
} from "@tileflow/static/client";
|
|
17
|
+
|
|
18
|
+
// src/static-map-request.ts
|
|
19
|
+
import {
|
|
20
|
+
requestStaticMapUntilReady,
|
|
21
|
+
stableStringify
|
|
22
|
+
} from "@tileflow/static/client";
|
|
9
23
|
var inFlightRequests = /* @__PURE__ */ new Map();
|
|
24
|
+
function createStaticMapRequestKey(input) {
|
|
25
|
+
return stableStringify({
|
|
26
|
+
createUrl: input.createUrl,
|
|
27
|
+
idempotencyKey: input.idempotencyKey,
|
|
28
|
+
sceneKey: input.request.sceneKey
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function resolveStaticMap(input) {
|
|
32
|
+
throwIfAborted(input.signal);
|
|
33
|
+
const requestKey = createStaticMapRequestKey(input);
|
|
34
|
+
let entry = inFlightRequests.get(requestKey);
|
|
35
|
+
if (!entry) {
|
|
36
|
+
const controller = new AbortController();
|
|
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
|
+
});
|
|
45
|
+
const created = {
|
|
46
|
+
controller,
|
|
47
|
+
consumers: 0,
|
|
48
|
+
promise,
|
|
49
|
+
settled: false
|
|
50
|
+
};
|
|
51
|
+
const settle = () => {
|
|
52
|
+
created.settled = true;
|
|
53
|
+
if (inFlightRequests.get(requestKey) === created) {
|
|
54
|
+
inFlightRequests.delete(requestKey);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
void promise.then(settle, settle);
|
|
58
|
+
inFlightRequests.set(requestKey, created);
|
|
59
|
+
entry = created;
|
|
60
|
+
}
|
|
61
|
+
entry.consumers += 1;
|
|
62
|
+
try {
|
|
63
|
+
return await raceWithAbort(entry.promise, input.signal);
|
|
64
|
+
} finally {
|
|
65
|
+
entry.consumers -= 1;
|
|
66
|
+
if (entry.consumers === 0 && !entry.settled) {
|
|
67
|
+
if (inFlightRequests.get(requestKey) === entry) {
|
|
68
|
+
inFlightRequests.delete(requestKey);
|
|
69
|
+
}
|
|
70
|
+
entry.controller.abort();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function raceWithAbort(operation, signal) {
|
|
75
|
+
if (!signal) return operation;
|
|
76
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const onAbort = () => {
|
|
79
|
+
cleanup();
|
|
80
|
+
reject(abortReason(signal));
|
|
81
|
+
};
|
|
82
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
83
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
84
|
+
operation.then(
|
|
85
|
+
(value) => {
|
|
86
|
+
cleanup();
|
|
87
|
+
resolve(value);
|
|
88
|
+
},
|
|
89
|
+
(error) => {
|
|
90
|
+
cleanup();
|
|
91
|
+
reject(error);
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function throwIfAborted(signal) {
|
|
97
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
98
|
+
}
|
|
99
|
+
function abortReason(signal) {
|
|
100
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
101
|
+
const error = new Error("Static map request was aborted");
|
|
102
|
+
error.name = "AbortError";
|
|
103
|
+
return error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/static-map.tsx
|
|
107
|
+
import { jsx } from "react/jsx-runtime";
|
|
10
108
|
function StaticMap({
|
|
11
109
|
alt = "",
|
|
110
|
+
attribution,
|
|
12
111
|
camera,
|
|
13
112
|
className,
|
|
113
|
+
captureId,
|
|
14
114
|
createUrl,
|
|
115
|
+
format,
|
|
15
116
|
imageStyle,
|
|
16
117
|
imageUrl,
|
|
118
|
+
idempotencyKey,
|
|
17
119
|
keepPreviousImage = false,
|
|
18
120
|
loading = "lazy",
|
|
19
121
|
map,
|
|
@@ -22,24 +124,65 @@ function StaticMap({
|
|
|
22
124
|
onReady,
|
|
23
125
|
overlays,
|
|
24
126
|
size,
|
|
25
|
-
style
|
|
127
|
+
style,
|
|
128
|
+
theme
|
|
26
129
|
}) {
|
|
27
130
|
const [result, setResult] = useState(
|
|
28
131
|
imageUrl ? imageResult(imageUrl) : null
|
|
29
132
|
);
|
|
30
133
|
const [error, setError] = useState(null);
|
|
134
|
+
const [captureState, setCaptureState] = useState("loading");
|
|
135
|
+
const imageLoadRunRef = useRef(0);
|
|
136
|
+
const imageRef = useRef(null);
|
|
137
|
+
const resolvedCaptureId = normalizeTileflowCaptureId(captureId);
|
|
31
138
|
const onErrorRef = useRef(onError);
|
|
32
139
|
const onReadyRef = useRef(onReady);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
140
|
+
const sceneInputKey = stableStringify2({ attribution, camera, format, map, overlays, size, theme });
|
|
141
|
+
const preparedSceneCandidate = useMemo(
|
|
142
|
+
() => prepareSceneRequest({ attribution, camera, format, map, overlays, size, theme }),
|
|
143
|
+
[attribution, camera, format, map, overlays, size, theme]
|
|
36
144
|
);
|
|
37
|
-
const
|
|
145
|
+
const preparedScene = useStablePreparedSceneRequest(preparedSceneCandidate);
|
|
146
|
+
const sceneKey = preparedScene.ok ? preparedScene.request.sceneKey : `invalid:${sceneInputKey}`;
|
|
147
|
+
const intentKey = imageUrl ? `image:${imageUrl}:${sceneKey}` : `create:${createUrl}:${idempotencyKey ?? ""}:${sceneKey}`;
|
|
148
|
+
const visibleResult = preparedScene.ok ? result : null;
|
|
149
|
+
const visibleError = preparedScene.ok ? error : preparedScene.error;
|
|
150
|
+
const effectiveCaptureState = preparedScene.ok ? captureState : "error";
|
|
151
|
+
useLayoutEffect(() => {
|
|
152
|
+
imageLoadRunRef.current += 1;
|
|
153
|
+
setCaptureState("loading");
|
|
154
|
+
const image = imageRef.current;
|
|
155
|
+
if (image?.complete) {
|
|
156
|
+
if (image.naturalWidth === 0) setCaptureState("error");
|
|
157
|
+
else void markImageReady(image);
|
|
158
|
+
}
|
|
159
|
+
}, [intentKey]);
|
|
160
|
+
async function markImageReady(image) {
|
|
161
|
+
const runId = ++imageLoadRunRef.current;
|
|
162
|
+
try {
|
|
163
|
+
if (typeof image.decode === "function") await image.decode();
|
|
164
|
+
} catch {
|
|
165
|
+
if (!image.complete || image.naturalWidth === 0) {
|
|
166
|
+
if (runId === imageLoadRunRef.current) setCaptureState("error");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (runId === imageLoadRunRef.current) setCaptureState("idle");
|
|
171
|
+
}
|
|
38
172
|
useEffect(() => {
|
|
39
173
|
onErrorRef.current = onError;
|
|
40
174
|
onReadyRef.current = onReady;
|
|
41
175
|
}, [onError, onReady]);
|
|
42
176
|
useEffect(() => {
|
|
177
|
+
let cancelled = false;
|
|
178
|
+
if (!preparedScene.ok) {
|
|
179
|
+
if (!keepPreviousImage) setResult(null);
|
|
180
|
+
setError(preparedScene.error);
|
|
181
|
+
imageLoadRunRef.current += 1;
|
|
182
|
+
setCaptureState("error");
|
|
183
|
+
onErrorRef.current?.(preparedScene.error);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
43
186
|
if (imageUrl) {
|
|
44
187
|
const nextResult = imageResult(imageUrl);
|
|
45
188
|
setResult(nextResult);
|
|
@@ -47,18 +190,29 @@ function StaticMap({
|
|
|
47
190
|
onReadyRef.current?.(nextResult);
|
|
48
191
|
return;
|
|
49
192
|
}
|
|
50
|
-
let cancelled = false;
|
|
51
193
|
if (!createUrl) {
|
|
52
194
|
return;
|
|
53
195
|
}
|
|
196
|
+
const validation = validateStaticMapIdempotencyKey(idempotencyKey ?? "");
|
|
197
|
+
if (!validation.ok) {
|
|
198
|
+
const keyError = new Error(`Invalid Static Maps idempotency key: ${validation.error}`);
|
|
199
|
+
if (!keepPreviousImage) setResult(null);
|
|
200
|
+
setError(keyError);
|
|
201
|
+
imageLoadRunRef.current += 1;
|
|
202
|
+
setCaptureState("error");
|
|
203
|
+
onErrorRef.current?.(keyError);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
54
206
|
if (!keepPreviousImage) {
|
|
55
207
|
setResult(null);
|
|
56
208
|
}
|
|
57
209
|
setError(null);
|
|
210
|
+
const requestController = new AbortController();
|
|
58
211
|
resolveStaticMap({
|
|
59
212
|
createUrl,
|
|
60
|
-
|
|
61
|
-
|
|
213
|
+
idempotencyKey: validation.key,
|
|
214
|
+
request: preparedScene.request,
|
|
215
|
+
signal: requestController.signal
|
|
62
216
|
}).then((nextResult) => {
|
|
63
217
|
if (cancelled) {
|
|
64
218
|
return;
|
|
@@ -75,17 +229,24 @@ function StaticMap({
|
|
|
75
229
|
setResult(null);
|
|
76
230
|
}
|
|
77
231
|
setError(normalizedError);
|
|
232
|
+
imageLoadRunRef.current += 1;
|
|
233
|
+
setCaptureState("error");
|
|
78
234
|
onErrorRef.current?.(normalizedError);
|
|
79
235
|
});
|
|
80
236
|
return () => {
|
|
81
237
|
cancelled = true;
|
|
238
|
+
requestController.abort();
|
|
82
239
|
};
|
|
83
|
-
}, [createUrl, imageUrl, keepPreviousImage,
|
|
240
|
+
}, [createUrl, idempotencyKey, imageUrl, keepPreviousImage, preparedScene]);
|
|
84
241
|
return /* @__PURE__ */ jsx(
|
|
85
242
|
"div",
|
|
86
243
|
{
|
|
87
|
-
"aria-busy": !
|
|
244
|
+
"aria-busy": !visibleResult && !visibleError,
|
|
88
245
|
className,
|
|
246
|
+
"data-tileflow-capture-id": resolvedCaptureId,
|
|
247
|
+
"data-tileflow-map": map,
|
|
248
|
+
"data-tileflow-theme": preparedScene.ok ? theme : void 0,
|
|
249
|
+
"data-tileflow-state": effectiveCaptureState,
|
|
89
250
|
style: {
|
|
90
251
|
aspectRatio: `${size.width} / ${size.height}`,
|
|
91
252
|
background: "#f4f4f2",
|
|
@@ -93,7 +254,7 @@ function StaticMap({
|
|
|
93
254
|
width: "100%",
|
|
94
255
|
...style
|
|
95
256
|
},
|
|
96
|
-
children:
|
|
257
|
+
children: visibleResult ? /* @__PURE__ */ jsx(
|
|
97
258
|
"img",
|
|
98
259
|
{
|
|
99
260
|
alt,
|
|
@@ -102,10 +263,16 @@ function StaticMap({
|
|
|
102
263
|
onError: () => {
|
|
103
264
|
const imageError = new Error("Static map image failed to load");
|
|
104
265
|
setError(imageError);
|
|
266
|
+
imageLoadRunRef.current += 1;
|
|
267
|
+
setCaptureState("error");
|
|
105
268
|
onErrorRef.current?.(imageError);
|
|
106
269
|
},
|
|
107
|
-
onLoad:
|
|
108
|
-
|
|
270
|
+
onLoad: (event) => {
|
|
271
|
+
onImageLoad?.(event);
|
|
272
|
+
void markImageReady(event.currentTarget);
|
|
273
|
+
},
|
|
274
|
+
ref: imageRef,
|
|
275
|
+
src: visibleResult.imageUrl,
|
|
109
276
|
style: {
|
|
110
277
|
display: "block",
|
|
111
278
|
height: "100%",
|
|
@@ -118,50 +285,36 @@ function StaticMap({
|
|
|
118
285
|
}
|
|
119
286
|
);
|
|
120
287
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
288
|
+
function prepareSceneRequest(scene) {
|
|
289
|
+
try {
|
|
290
|
+
return { ok: true, request: prepareStaticMapRequest(scene) };
|
|
291
|
+
} catch (error) {
|
|
292
|
+
return {
|
|
293
|
+
error: error instanceof Error ? error : new Error("Invalid Tileflow static scene"),
|
|
294
|
+
ok: false
|
|
295
|
+
};
|
|
125
296
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
return await response.json();
|
|
137
|
-
}).finally(() => {
|
|
138
|
-
inFlightRequests.delete(input.requestKey);
|
|
139
|
-
});
|
|
140
|
-
inFlightRequests.set(input.requestKey, promise);
|
|
141
|
-
return promise;
|
|
297
|
+
}
|
|
298
|
+
function useStablePreparedSceneRequest(value) {
|
|
299
|
+
const valueRef = useRef(value);
|
|
300
|
+
const current = valueRef.current;
|
|
301
|
+
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);
|
|
302
|
+
if (!equivalent) {
|
|
303
|
+
valueRef.current = value;
|
|
304
|
+
}
|
|
305
|
+
return valueRef.current;
|
|
142
306
|
}
|
|
143
307
|
function imageResult(imageUrl) {
|
|
144
308
|
return {
|
|
145
309
|
cached: true,
|
|
146
310
|
hash: "",
|
|
147
311
|
imageUrl,
|
|
148
|
-
|
|
312
|
+
operationId: null,
|
|
313
|
+
remainingUnits: null,
|
|
314
|
+
status: "ready",
|
|
315
|
+
unitCost: 0
|
|
149
316
|
};
|
|
150
317
|
}
|
|
151
|
-
function stableStringify(value) {
|
|
152
|
-
return JSON.stringify(sortJson(value));
|
|
153
|
-
}
|
|
154
|
-
function sortJson(value) {
|
|
155
|
-
if (Array.isArray(value)) {
|
|
156
|
-
return value.map(sortJson);
|
|
157
|
-
}
|
|
158
|
-
if (value && typeof value === "object") {
|
|
159
|
-
return Object.fromEntries(
|
|
160
|
-
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, sortJson(entry)])
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
return value;
|
|
164
|
-
}
|
|
165
318
|
export {
|
|
166
319
|
StaticMap
|
|
167
320
|
};
|
package/package.json
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tileflow/react",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.30",
|
|
4
|
+
"license": "Apache-2.0",
|
|
4
5
|
"description": "React components for rendering Tileflow maps with MapLibre.",
|
|
5
6
|
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
6
8
|
"homepage": "https://tileflow.dev",
|
|
7
9
|
"repository": {
|
|
8
10
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/tileflow/tileflow.git",
|
|
11
|
+
"url": "git+https://github.com/tileflow/tileflow-sdk.git",
|
|
10
12
|
"directory": "packages/react"
|
|
11
13
|
},
|
|
12
14
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/tileflow/tileflow/issues"
|
|
15
|
+
"url": "https://github.com/tileflow/tileflow-sdk/issues"
|
|
14
16
|
},
|
|
15
17
|
"keywords": [
|
|
16
18
|
"tileflow",
|
|
@@ -22,7 +24,8 @@
|
|
|
22
24
|
"main": "./dist/index.js",
|
|
23
25
|
"types": "./dist/index.d.ts",
|
|
24
26
|
"files": [
|
|
25
|
-
"dist"
|
|
27
|
+
"dist",
|
|
28
|
+
"LICENSE"
|
|
26
29
|
],
|
|
27
30
|
"exports": {
|
|
28
31
|
".": {
|
|
@@ -40,24 +43,26 @@
|
|
|
40
43
|
"access": "public"
|
|
41
44
|
},
|
|
42
45
|
"dependencies": {
|
|
43
|
-
"@tileflow/
|
|
44
|
-
"@tileflow/
|
|
46
|
+
"@tileflow/core": ">=0.1.0-alpha.35 <0.1.0-beta.0",
|
|
47
|
+
"@tileflow/interactions": ">=0.1.0-alpha.5 <0.1.0-beta.0",
|
|
48
|
+
"@tileflow/static": ">=0.1.0-alpha.28 <0.1.0-beta.0"
|
|
45
49
|
},
|
|
46
50
|
"peerDependencies": {
|
|
47
|
-
"maplibre-gl": ">=
|
|
48
|
-
"react": ">=18",
|
|
49
|
-
"react-dom": ">=18"
|
|
51
|
+
"maplibre-gl": ">=6.4.1 <7",
|
|
52
|
+
"react": ">=18 <20",
|
|
53
|
+
"react-dom": ">=18 <20"
|
|
50
54
|
},
|
|
51
55
|
"devDependencies": {
|
|
52
|
-
"
|
|
53
|
-
"react": "^19.
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
56
|
+
"@types/react": "^19.3.0",
|
|
57
|
+
"@types/react-dom": "^19.3.0",
|
|
58
|
+
"maplibre-gl": "6.13.0",
|
|
59
|
+
"react": "^19.3.0",
|
|
60
|
+
"react-dom": "^19.3.0"
|
|
57
61
|
},
|
|
58
62
|
"scripts": {
|
|
59
|
-
"build": "tsup
|
|
60
|
-
"
|
|
63
|
+
"build": "tsup",
|
|
64
|
+
"verify": "pnpm run build && tsx --test test/*.test.ts",
|
|
65
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.contract.json",
|
|
61
66
|
"lint": "tsc -p tsconfig.json --noEmit"
|
|
62
67
|
}
|
|
63
68
|
}
|