@tileflow/react 0.1.0-alpha.3 → 0.1.0-alpha.31

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/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 { StaticSceneInput, StaticMapResult } from '@tileflow/static';
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>["loading"];
12
+ loading?: ImgHTMLAttributes<HTMLImageElement>['loading'];
11
13
  onError?: (error: Error) => void;
12
- onImageLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
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 { jsx } from "react/jsx-runtime";
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 sceneKey = useMemo(
34
- () => stableStringify({ camera, map, overlays, size }),
35
- [camera, map, overlays, size]
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 requestKey = imageUrl ? `image:${imageUrl}` : `create:${createUrl}:${sceneKey}`;
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
- requestKey,
61
- scene: JSON.parse(sceneKey)
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, requestKey, sceneKey]);
240
+ }, [createUrl, idempotencyKey, imageUrl, keepPreviousImage, preparedScene]);
84
241
  return /* @__PURE__ */ jsx(
85
242
  "div",
86
243
  {
87
- "aria-busy": !result && !error,
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: result ? /* @__PURE__ */ jsx(
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: onImageLoad,
108
- src: result.imageUrl,
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
- async function resolveStaticMap(input) {
122
- const existing = inFlightRequests.get(input.requestKey);
123
- if (existing) {
124
- return existing;
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
- const promise = fetch(input.createUrl, {
127
- body: JSON.stringify(input.scene),
128
- headers: {
129
- "Content-Type": "application/json"
130
- },
131
- method: "POST"
132
- }).then(async (response) => {
133
- if (!response.ok) {
134
- throw new Error(`Static map endpoint failed: ${response.status}`);
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
- status: "ready"
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",
3
+ "version": "0.1.0-alpha.31",
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/static": "0.1.0-alpha.3",
44
- "@tileflow/core": "0.1.0-alpha.3"
46
+ "@tileflow/core": ">=0.1.0-alpha.36 <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": ">=5",
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
- "maplibre-gl": "^5.24.0",
53
- "react": "^19.2.7",
54
- "react-dom": "^19.2.7",
55
- "@types/react": "^19.2.17",
56
- "@types/react-dom": "^19.2.3"
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 src/index.ts src/static.ts --format esm --dts --clean --external react --external react-dom --external maplibre-gl",
60
- "typecheck": "tsc -p tsconfig.json --noEmit",
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
  }