@tileflow/react 0.1.0-alpha.2 → 0.1.0-alpha.21

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, camera, className, captureId, createUrl, 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,119 @@
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 = "",
12
110
  camera,
13
111
  className,
112
+ captureId,
14
113
  createUrl,
15
114
  imageStyle,
16
115
  imageUrl,
116
+ idempotencyKey,
17
117
  keepPreviousImage = false,
18
118
  loading = "lazy",
19
119
  map,
@@ -22,24 +122,65 @@ function StaticMap({
22
122
  onReady,
23
123
  overlays,
24
124
  size,
25
- style
125
+ style,
126
+ theme
26
127
  }) {
27
128
  const [result, setResult] = useState(
28
129
  imageUrl ? imageResult(imageUrl) : null
29
130
  );
30
131
  const [error, setError] = useState(null);
132
+ const [captureState, setCaptureState] = useState("loading");
133
+ const imageLoadRunRef = useRef(0);
134
+ const imageRef = useRef(null);
135
+ const resolvedCaptureId = normalizeTileflowCaptureId(captureId);
31
136
  const onErrorRef = useRef(onError);
32
137
  const onReadyRef = useRef(onReady);
33
- const sceneKey = useMemo(
34
- () => stableStringify({ camera, map, overlays, size }),
35
- [camera, map, overlays, size]
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]
36
142
  );
37
- const requestKey = imageUrl ? `image:${imageUrl}` : `create:${createUrl}:${sceneKey}`;
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";
149
+ useLayoutEffect(() => {
150
+ imageLoadRunRef.current += 1;
151
+ setCaptureState("loading");
152
+ const image = imageRef.current;
153
+ if (image?.complete) {
154
+ if (image.naturalWidth === 0) setCaptureState("error");
155
+ else void markImageReady(image);
156
+ }
157
+ }, [intentKey]);
158
+ async function markImageReady(image) {
159
+ const runId = ++imageLoadRunRef.current;
160
+ try {
161
+ if (typeof image.decode === "function") await image.decode();
162
+ } catch {
163
+ if (!image.complete || image.naturalWidth === 0) {
164
+ if (runId === imageLoadRunRef.current) setCaptureState("error");
165
+ return;
166
+ }
167
+ }
168
+ if (runId === imageLoadRunRef.current) setCaptureState("idle");
169
+ }
38
170
  useEffect(() => {
39
171
  onErrorRef.current = onError;
40
172
  onReadyRef.current = onReady;
41
173
  }, [onError, onReady]);
42
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
+ }
43
184
  if (imageUrl) {
44
185
  const nextResult = imageResult(imageUrl);
45
186
  setResult(nextResult);
@@ -47,18 +188,29 @@ function StaticMap({
47
188
  onReadyRef.current?.(nextResult);
48
189
  return;
49
190
  }
50
- let cancelled = false;
51
191
  if (!createUrl) {
52
192
  return;
53
193
  }
194
+ const validation = validateStaticMapIdempotencyKey(idempotencyKey ?? "");
195
+ if (!validation.ok) {
196
+ const keyError = new Error(`Invalid Static Maps idempotency key: ${validation.error}`);
197
+ if (!keepPreviousImage) setResult(null);
198
+ setError(keyError);
199
+ imageLoadRunRef.current += 1;
200
+ setCaptureState("error");
201
+ onErrorRef.current?.(keyError);
202
+ return;
203
+ }
54
204
  if (!keepPreviousImage) {
55
205
  setResult(null);
56
206
  }
57
207
  setError(null);
208
+ const requestController = new AbortController();
58
209
  resolveStaticMap({
59
210
  createUrl,
60
- requestKey,
61
- scene: JSON.parse(sceneKey)
211
+ idempotencyKey: validation.key,
212
+ request: preparedScene.request,
213
+ signal: requestController.signal
62
214
  }).then((nextResult) => {
63
215
  if (cancelled) {
64
216
  return;
@@ -75,17 +227,24 @@ function StaticMap({
75
227
  setResult(null);
76
228
  }
77
229
  setError(normalizedError);
230
+ imageLoadRunRef.current += 1;
231
+ setCaptureState("error");
78
232
  onErrorRef.current?.(normalizedError);
79
233
  });
80
234
  return () => {
81
235
  cancelled = true;
236
+ requestController.abort();
82
237
  };
83
- }, [createUrl, imageUrl, keepPreviousImage, requestKey, sceneKey]);
238
+ }, [createUrl, idempotencyKey, imageUrl, keepPreviousImage, preparedScene]);
84
239
  return /* @__PURE__ */ jsx(
85
240
  "div",
86
241
  {
87
- "aria-busy": !result && !error,
242
+ "aria-busy": !visibleResult && !visibleError,
88
243
  className,
244
+ "data-tileflow-capture-id": resolvedCaptureId,
245
+ "data-tileflow-map": map,
246
+ "data-tileflow-theme": preparedScene.ok ? theme : void 0,
247
+ "data-tileflow-state": effectiveCaptureState,
89
248
  style: {
90
249
  aspectRatio: `${size.width} / ${size.height}`,
91
250
  background: "#f4f4f2",
@@ -93,7 +252,7 @@ function StaticMap({
93
252
  width: "100%",
94
253
  ...style
95
254
  },
96
- children: result ? /* @__PURE__ */ jsx(
255
+ children: visibleResult ? /* @__PURE__ */ jsx(
97
256
  "img",
98
257
  {
99
258
  alt,
@@ -102,10 +261,16 @@ function StaticMap({
102
261
  onError: () => {
103
262
  const imageError = new Error("Static map image failed to load");
104
263
  setError(imageError);
264
+ imageLoadRunRef.current += 1;
265
+ setCaptureState("error");
105
266
  onErrorRef.current?.(imageError);
106
267
  },
107
- onLoad: onImageLoad,
108
- src: result.imageUrl,
268
+ onLoad: (event) => {
269
+ onImageLoad?.(event);
270
+ void markImageReady(event.currentTarget);
271
+ },
272
+ ref: imageRef,
273
+ src: visibleResult.imageUrl,
109
274
  style: {
110
275
  display: "block",
111
276
  height: "100%",
@@ -118,50 +283,36 @@ function StaticMap({
118
283
  }
119
284
  );
120
285
  }
121
- async function resolveStaticMap(input) {
122
- const existing = inFlightRequests.get(input.requestKey);
123
- if (existing) {
124
- return existing;
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
+ };
125
294
  }
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;
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;
142
304
  }
143
305
  function imageResult(imageUrl) {
144
306
  return {
145
307
  cached: true,
146
308
  hash: "",
147
309
  imageUrl,
148
- status: "ready"
310
+ operationId: null,
311
+ remainingUnits: null,
312
+ status: "ready",
313
+ unitCost: 0
149
314
  };
150
315
  }
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
316
  export {
166
317
  StaticMap
167
318
  };
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@tileflow/react",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.21",
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,11 @@
22
24
  "main": "./dist/index.js",
23
25
  "types": "./dist/index.d.ts",
24
26
  "files": [
25
- "dist"
27
+ "dist",
28
+ "LICENSE",
29
+ "NOTICE",
30
+ "GENERATED_OUTPUT_LICENSE.md",
31
+ "TRADEMARKS.md"
26
32
  ],
27
33
  "exports": {
28
34
  ".": {
@@ -40,24 +46,26 @@
40
46
  "access": "public"
41
47
  },
42
48
  "dependencies": {
43
- "@tileflow/static": "0.1.0-alpha.2",
44
- "@tileflow/core": "0.1.0-alpha.2"
49
+ "@tileflow/core": ">=0.1.0-alpha.22 <0.1.0-beta.0",
50
+ "@tileflow/interactions": ">=0.1.0-alpha.1 <0.1.0-beta.0",
51
+ "@tileflow/static": ">=0.1.0-alpha.18 <0.1.0-beta.0"
45
52
  },
46
53
  "peerDependencies": {
47
- "maplibre-gl": ">=5",
48
- "react": ">=18",
49
- "react-dom": ">=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
- "react-dom": "^19.2.7",
55
- "@types/react": "^19.2.17",
56
- "@types/react-dom": "^19.2.3"
63
+ "react-dom": "^19.2.7"
57
64
  },
58
65
  "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",
66
+ "build": "tsup",
67
+ "verify": "pnpm run build && tsx --test test/*.test.ts",
68
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.contract.json",
61
69
  "lint": "tsc -p tsconfig.json --noEmit"
62
70
  }
63
71
  }