@tileflow/react-native 0.1.0-alpha.5 → 0.1.0-alpha.7

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 CHANGED
@@ -93,19 +93,48 @@ Maps without this capability ignore the option; omitting it keeps the selected t
93
93
  Changing it uses the existing theme transition. `themeBlend` takes precedence. See the
94
94
  [authoring contract](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/README.md#style-with-themes-and-semantic-modules).
95
95
 
96
- `themeTransition={{duration: 450}}` cross-fades theme changes: a snapshot of the drawn map covers it, follows the camera and fades out once the new theme is drawn. `themeBlend={{themes: ['night', 'dusk', 'day'], position}}` shows a continuous blend of the map's themes instead of `theme`; position 0 is the first theme and 1.5 is halfway from the second to the third:
96
+ Theme and `timeOfDay` changes cross-fade over 450 ms by default: a snapshot of the drawn map covers it, follows the camera and fades out once the new theme is drawn. Use `themeTransition={{duration: 700}}` to customize the duration, or `duration: 0` for an immediate change. `themeBlend={{themes: ['night', 'dusk', 'day'], position}}` shows a continuous blend of the map's themes instead of `theme`; position 0 is the first theme and 1.5 is halfway from the second to the third:
97
97
 
98
98
  ```tsx
99
- <Map
100
- source={source}
101
- themeTransition={{duration: 450}}
102
- themeBlend={{themes: ['night', 'dusk', 'day'], position}}
103
- style={{flex: 1}}
104
- />
99
+ <Map source={source} themeBlend={{themes: ['night', 'dusk', 'day'], position}} style={{flex: 1}} />
105
100
  ```
106
101
 
107
102
  The first blend of a set of themes prepares every theme's style and replaces the native style once; moving `position` later changes the map without loading. Removing `themeBlend` returns to `theme`. Both keep the same native Map, context and session, and `ready` events wait for the fade. The device's reduce-motion setting changes at once. See [theme transitions and blends](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/theme-transitions-and-blends.md#react-native) for what follows the position on native maps.
108
103
 
104
+ ## Boundary choropleths
105
+
106
+ A map that declares `boundaryAreas` and a `boundaryChoropleth()` without compiled `values` reads
107
+ its colors from feature-state. `boundaryFeatureState` sets that state from the application, with
108
+ the same rows and options as `useBoundaryFeatureState` on the web: rows keyed by the source's
109
+ feature ID (`id`, or `ref` when the map promotes it), `labels` and `state`:
110
+
111
+ <!-- docs:check -->
112
+
113
+ ```tsx
114
+ import {Map, type MapSource} from '@tileflow/react-native';
115
+ import {useMemo} from 'react';
116
+
117
+ export function UnemploymentMap({
118
+ source,
119
+ rates,
120
+ }: {
121
+ source: MapSource;
122
+ rates: ReadonlyMap<number, number>;
123
+ }) {
124
+ // Memoize the values: each new object replaces the previous ones.
125
+ const values = useMemo(
126
+ () => ({joinBy: 'id' as const, rows: [...rates].map(([key, value]) => ({key, value}))}),
127
+ [rates],
128
+ );
129
+ return <Map source={source} boundaryFeatureState={{values}} style={{flex: 1}} />;
130
+ }
131
+ ```
132
+
133
+ Values survive theme changes: they are set again once the new style has loaded. `values: null`
134
+ removes them. Rows keyed by codes the polygons do not carry are joined before they reach the app,
135
+ on a server or at build time; see
136
+ [boundary areas and choropleths](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/boundary-areas-and-choropleths.md#join-by-codes-the-polygons-do-not-carry).
137
+
109
138
  ## Camera
110
139
 
111
140
  The existing mount-stable camera contract is preserved. Use either an initial camera seed or complete controlled camera ownership for the lifetime of one mounted Map:
@@ -174,10 +203,39 @@ The public lifecycle surface remains:
174
203
  - `onReadinessChange`;
175
204
  - `onThemeChange`;
176
205
  - controlled `onViewChange`;
177
- - `ref.getSourceState()`.
206
+ - `ref.getSourceState()`;
207
+ - `ref.retry()`.
178
208
 
179
209
  `getSourceState()` returns only the last safe projected source snapshot. Errors use existing bounded codes/fields/kinds. No URL, response body, native event, credential, grant, session object, native handle or raw cause enters public callbacks, refs or serializable state.
180
210
 
211
+ `onError` reports `source-error`, `renderer-error` and, for Hosted session delivery, `session-error`. A `session-error` means Tileflow refused the session: the mobile credential was rejected (`NATIVE_SESSION_CREDENTIAL_REJECTED`), it does not authorize the Map (`NATIVE_SESSION_MAP_MISMATCH`), the Map was not found (`NATIVE_SESSION_MAP_NOT_FOUND`) or its view or spending limit was reached (`NATIVE_SESSION_QUOTA_EXCEEDED`). It can arrive after the first load, when a renewal is refused. The map keeps what it has drawn but loads nothing new until you call `ref.retry()` or change the source:
212
+
213
+ ```tsx
214
+ import {Map, type MapRef} from '@tileflow/react-native';
215
+ import {useRef} from 'react';
216
+
217
+ export function StoreMap() {
218
+ const map = useRef<MapRef>(null);
219
+ return (
220
+ <Map
221
+ ref={map}
222
+ source={{map: 'stores', manifestUrl: 'https://maps.example.com/tileflow/manifest.json'}}
223
+ onError={(event) => {
224
+ if (
225
+ event.type === 'session-error' &&
226
+ event.error.code === 'NATIVE_SESSION_QUOTA_EXCEEDED'
227
+ ) {
228
+ // Show your own notice; call map.current?.retry() once the limit is raised.
229
+ }
230
+ }}
231
+ style={{flex: 1}}
232
+ />
233
+ );
234
+ }
235
+ ```
236
+
237
+ Service outages, timeouts and rate limiting are not session errors. The map keeps using its current grant while it is valid and requests another after an increasing delay, from one second up to one minute, or after the `Retry-After` the service sends. A first load that failed this way prepares again on its own. `ref.retry()` also loads again after a source or renderer error, and changes nothing while the map is shown.
238
+
181
239
  ## Native integration
182
240
 
183
241
  Autolinking registers the private Android/iOS modules used by the mounted lifecycle. Android depends on the exact MapLibre React Native project and MapLibre Native 13.6.1. iOS uses the existing MapLibre React Native CocoaPod plus the exact MapLibre Native 6.31.0 Swift Package Manager product.
@@ -6,7 +6,10 @@ internal object AdmissionBootstrapLimits {
6
6
  const val QUEUE = 32
7
7
  const val REQUEST_BYTES = 2048
8
8
  const val RESPONSE_BYTES = 65536
9
- const val TIMEOUT_MS = 30000L
9
+ const val RETRY_AFTER_CHARACTERS = 64
10
+ // Well inside the 30 s ticket and batch deadlines, so a stalled renewal fails its batch
11
+ // instead of letting the context retire under it.
12
+ const val TIMEOUT_MS = 10000L
10
13
  }
11
14
 
12
15
  internal class AdmissionBootstrapRequest(
@@ -18,7 +21,7 @@ internal class AdmissionBootstrapRequest(
18
21
  override fun toString() = "AdmissionBootstrapRequest(redacted)"
19
22
  }
20
23
 
21
- internal class AdmissionBootstrapReply(val status: Int, val cacheControl: String, val body: ByteArray) {
24
+ internal class AdmissionBootstrapReply(val status: Int, val cacheControl: String, val body: ByteArray, val retryAfter: String = "") {
22
25
  override fun toString() = "AdmissionBootstrapReply(redacted)"
23
26
  }
24
27
 
@@ -86,7 +89,8 @@ internal class AdmissionBootstrap(
86
89
  work.network = network.start(AdmissionBootstrapRequest(url, credential, body) { mayStart(work) }) { reply ->
87
90
  scheduler.dispatch {
88
91
  val valid = mayStart(work) && reply != null && reply.status in 100..599 &&
89
- reply.cacheControl.length <= 1024 && reply.body.size <= AdmissionBootstrapLimits.RESPONSE_BYTES
92
+ reply.cacheControl.length <= 1024 && reply.retryAfter.length <= AdmissionBootstrapLimits.RETRY_AFTER_CHARACTERS &&
93
+ reply.body.size <= AdmissionBootstrapLimits.RESPONSE_BYTES
90
94
  finish(work, if (valid) reply else null)
91
95
  }
92
96
  }
@@ -55,6 +55,8 @@ internal class AdmissionBootstrapOkHttpNetwork : AdmissionBootstrapNetwork {
55
55
  val reply = try {
56
56
  response.use {
57
57
  val cacheControl = it.header("cache-control") ?: ""
58
+ // An oversized Retry-After is ignored rather than failing the reply.
59
+ val retryAfter = (it.header("retry-after") ?: "").takeIf { value -> value.length <= AdmissionBootstrapLimits.RETRY_AFTER_CHARACTERS } ?: ""
58
60
  val body = it.body
59
61
  if (cacheControl.length > 1024 || body == null || body.contentLength() > AdmissionBootstrapLimits.RESPONSE_BYTES) throw IOException("Invalid bootstrap response")
60
62
  val bytes = ByteArrayOutputStream()
@@ -67,7 +69,7 @@ internal class AdmissionBootstrapOkHttpNetwork : AdmissionBootstrapNetwork {
67
69
  bytes.write(buffer, 0, count)
68
70
  }
69
71
  }
70
- AdmissionBootstrapReply(it.code, cacheControl, bytes.toByteArray())
72
+ AdmissionBootstrapReply(it.code, cacheControl, bytes.toByteArray(), retryAfter)
71
73
  }
72
74
  } catch (_: Exception) { null }
73
75
  complete(reply)
@@ -155,8 +155,10 @@ internal class AdmissionEngine(
155
155
  work = item
156
156
  context.work[item.id] = item
157
157
  item.timer = scheduler.after(AdmissionLimits.WAIT_MS) {
158
- if (context.batch?.tickets?.contains(item.id) == true) retire(context.id)
159
- else fail(item)
158
+ // A slow renewal fails only this request. JavaScript still answers its batch, and the
159
+ // batch deadline alone retires a context whose JavaScript stopped answering.
160
+ if (context.batch?.tickets?.contains(item.id) == true) event(context, "cancel", mapOf("tickets" to listOf(item.id)))
161
+ fail(item)
160
162
  }
161
163
  scheduler.dispatch { drain(context) }
162
164
  }
@@ -125,7 +125,7 @@ class TileflowNativeAdmissionModule(context: ReactApplicationContext) : ReactCon
125
125
  (bootstrapRequests ?: invalid()).start(context, request, url, credential, body) { reply ->
126
126
  if (reply == null) reject(promise)
127
127
  else promise.resolve(Arguments.makeNativeMap(mapOf(
128
- "status" to reply.status, "cacheControl" to reply.cacheControl,
128
+ "status" to reply.status, "cacheControl" to reply.cacheControl, "retryAfter" to reply.retryAfter,
129
129
  "bodyBase64" to Base64.encodeToString(reply.body, Base64.NO_WRAP))))
130
130
  }
131
131
  } catch (_: Exception) { reject(promise) }
@@ -1,7 +1,7 @@
1
1
  import { MapProps as MapProps$1 } from '@maplibre/maplibre-react-native';
2
2
  import { ReactElement, ReactNode, Ref } from 'react';
3
3
  import { ViewProps } from 'react-native';
4
- import { TileflowNativeInitialView, TileflowNativeInitialViewOptions, TileflowNativeSource, TileflowNativeSourceOptions, TileflowNativeSourceState, TileflowNativeSourceError } from '@tileflow/core/native';
4
+ import { TileflowNativeInitialView, TileflowNativeInitialViewOptions, TileflowNativeSource, TileflowNativeSourceOptions, TileflowBoundaryValues, TileflowBoundaryFamily, TileflowNativeSourceState, TileflowNativeSourceError } from '@tileflow/core/native';
5
5
  import { TileflowAnnotation, TileflowInteractionBinding, TileflowInteractionEvent, TileflowResolvedAnnotationTarget, TileflowResolvedPoiFeatureTarget, TileflowInteractionDiagnostic, TileflowAnnotationViewContext } from '@tileflow/interactions';
6
6
 
7
7
  /** Marker composition only. Selection presentation and native handles are not part of this context. */
@@ -28,12 +28,12 @@ type MapView = TileflowNativeInitialView;
28
28
  /** Portable composition inputs, independent from source acquisition. */
29
29
  type MapInitialViewInputs = TileflowNativeInitialViewOptions;
30
30
  /**
31
- * How a theme change appears. With a `duration`, a snapshot of the current frame covers the map,
31
+ * How a theme change appears. By default, a snapshot of the current frame covers the map,
32
32
  * follows the camera, and fades out once the new theme is drawn. The device's reduce-motion
33
33
  * setting changes at once.
34
34
  */
35
35
  type MapThemeTransition = Readonly<{
36
- /** Cross-fade length in milliseconds, from 0 (the default, an immediate change) to 5000. */
36
+ /** Cross-fade length in milliseconds, from 0 (immediate) to 5000. Defaults to 450. */
37
37
  duration?: number;
38
38
  }>;
39
39
  /**
@@ -58,6 +58,22 @@ type MapSourceProps = Readonly<{
58
58
  */
59
59
  themeBlend?: MapThemeBlend;
60
60
  }>;
61
+ /**
62
+ * Application values on the map's Boundaries areas, for a choropleth that reads them with
63
+ * `feature-state`. The same rows and options as `useBoundaryFeatureState` on the web: rows keyed by
64
+ * the source's feature ID (`id`, or `ref` when the map promotes it), joined with
65
+ * `joinBoundaryRows` when the data carries other codes. Values survive theme changes; `null`
66
+ * removes them.
67
+ */
68
+ type MapBoundaryFeatureState = Readonly<{
69
+ values: TileflowBoundaryValues | null;
70
+ /** Defaults to `boundaries`. */
71
+ family?: TileflowBoundaryFamily;
72
+ /** Also set the values on `boundary_label` points, for labels styled by value. */
73
+ labels?: boolean;
74
+ /** Feature-state key; it must match the choropleth's `state`. Defaults to `value`. */
75
+ state?: string;
76
+ }>;
61
77
  type AllowedMapOptions = 'dragPan' | 'touchZoom' | 'doubleTapZoom' | 'doubleTapHoldZoom' | 'touchRotate' | 'touchPitch' | 'compass' | 'compassHiddenFacingNorth' | 'scaleBar';
62
78
  type OwnedMapOptions = Exclude<keyof MapProps$1, AllowedMapOptions> | 'container' | 'styleURL' | 'styleUrl' | 'transformRequest' | 'requestTransform' | 'requestHeaders';
63
79
  /** A positive allowlist. Style, interception, view and lifecycle remain adapter-owned. */
@@ -69,6 +85,7 @@ type MapPresentationProps = Readonly<{
69
85
  style?: ViewProps['style'];
70
86
  testID?: ViewProps['testID'];
71
87
  mapOptions?: MapOptions;
88
+ boundaryFeatureState?: MapBoundaryFeatureState;
72
89
  }>;
73
90
  type TileflowReady = Extract<TileflowNativeSourceState, {
74
91
  status: 'ready';
@@ -92,9 +109,23 @@ type MapSourceState = Readonly<{
92
109
  generation: number;
93
110
  error: MapSourceError;
94
111
  }>;
95
- /** A synchronous read of the last source snapshot. Does not trigger loading or native commands. */
112
+ /**
113
+ * The Hosted session stopped: its credential, Map or quota was rejected. The map keeps what it has
114
+ * drawn and loads nothing new until `ref.retry()` or another source. Service outages and rate
115
+ * limits retry on their own and are not reported.
116
+ */
117
+ type MapSessionError = Readonly<{
118
+ code: 'NATIVE_SESSION_CREDENTIAL_REJECTED' | 'NATIVE_SESSION_MAP_MISMATCH' | 'NATIVE_SESSION_MAP_NOT_FOUND' | 'NATIVE_SESSION_QUOTA_EXCEEDED' | 'NATIVE_SESSION_INPUT_INVALID';
119
+ kind: 'terminal';
120
+ }>;
96
121
  type MapRef = Readonly<{
122
+ /** A synchronous read of the last source snapshot. Does not trigger loading or native commands. */
97
123
  getSourceState(): MapSourceState | undefined;
124
+ /**
125
+ * Loads again a map that stopped: after a `session-error` once its cause is resolved, for
126
+ * example a raised quota, or after a source or renderer error. A shown map is unaffected.
127
+ */
128
+ retry(): void;
98
129
  }>;
99
130
  /** The current style was accepted by the renderer; this alone does not establish a rendered frame. */
100
131
  type MapLoadEvent = Readonly<{
@@ -110,6 +141,10 @@ type MapErrorEvent = Readonly<{
110
141
  }> | Readonly<{
111
142
  type: 'renderer-error';
112
143
  generation: number;
144
+ }> | Readonly<{
145
+ type: 'session-error';
146
+ generation: number;
147
+ error: MapSessionError;
113
148
  }>;
114
149
  /** The renderer owner must confirm readiness; a ready manifest alone is insufficient. */
115
150
  type MapReadinessChangeEvent = Readonly<{
@@ -168,4 +203,4 @@ type MapCameraProps = Readonly<{
168
203
  /** Complete Tileflow source, presentation, mount-stable camera and portable interaction inputs. */
169
204
  type MapProps<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = MapBaseProps & MapCameraProps & MapInteractionProps<TAnnotation>;
170
205
 
171
- export type { MapThemeTransition as A, MapView as M, MapViewChangeEvent as a, MapInitialViewInputs as b, MapCameraProps as c, MapProps as d, MapBaseProps as e, MapColorScheme as f, MapErrorEvent as g, MapEventHandlers as h, MapInteractionEvent as i, MapInteractionProps as j, MapLoadEvent as k, MapMarkerRenderContext as l, MapMarkerRenderer as m, MapOptions as n, MapPresentationProps as o, MapReadinessChangeEvent as p, MapRef as q, MapSelection as r, MapSource as s, MapSourceError as t, MapSourceProps as u, MapSourceState as v, MapTheme as w, MapThemeBlend as x, MapThemeChangeEvent as y, MapThemeSelection as z };
206
+ export type { MapThemeChangeEvent as A, MapThemeSelection as B, MapThemeTransition as C, MapView as M, MapViewChangeEvent as a, MapInitialViewInputs as b, MapCameraProps as c, MapProps as d, MapBaseProps as e, MapBoundaryFeatureState as f, MapColorScheme as g, MapErrorEvent as h, MapEventHandlers as i, MapInteractionEvent as j, MapInteractionProps as k, MapLoadEvent as l, MapMarkerRenderContext as m, MapMarkerRenderer as n, MapOptions as o, MapPresentationProps as p, MapReadinessChangeEvent as q, MapRef as r, MapSelection as s, MapSessionError as t, MapSource as u, MapSourceError as v, MapSourceProps as w, MapSourceState as x, MapTheme as y, MapThemeBlend as z };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { TileflowAnnotation } from '@tileflow/interactions';
3
3
  export { TileflowAnnotation, TileflowInteractionBinding, TileflowInteractionCoordinate, TileflowInteractionDiagnostic, TileflowPoiCategory, TileflowResolvedAnnotationTarget, TileflowResolvedPoiFeatureTarget } from '@tileflow/interactions';
4
- import { d as MapProps } from './contract-Bp-Im7tj.js';
5
- export { e as MapBaseProps, c as MapCameraProps, f as MapColorScheme, g as MapErrorEvent, h as MapEventHandlers, b as MapInitialViewInputs, i as MapInteractionEvent, j as MapInteractionProps, k as MapLoadEvent, l as MapMarkerRenderContext, m as MapMarkerRenderer, n as MapOptions, o as MapPresentationProps, p as MapReadinessChangeEvent, q as MapRef, r as MapSelection, s as MapSource, t as MapSourceError, u as MapSourceProps, v as MapSourceState, w as MapTheme, x as MapThemeBlend, y as MapThemeChangeEvent, z as MapThemeSelection, A as MapThemeTransition, M as MapView, a as MapViewChangeEvent } from './contract-Bp-Im7tj.js';
4
+ import { d as MapProps } from './contract-BaK24QZD.js';
5
+ export { e as MapBaseProps, f as MapBoundaryFeatureState, c as MapCameraProps, g as MapColorScheme, h as MapErrorEvent, i as MapEventHandlers, b as MapInitialViewInputs, j as MapInteractionEvent, k as MapInteractionProps, l as MapLoadEvent, m as MapMarkerRenderContext, n as MapMarkerRenderer, o as MapOptions, p as MapPresentationProps, q as MapReadinessChangeEvent, r as MapRef, s as MapSelection, t as MapSessionError, u as MapSource, v as MapSourceError, w as MapSourceProps, x as MapSourceState, y as MapTheme, z as MapThemeBlend, A as MapThemeChangeEvent, B as MapThemeSelection, C as MapThemeTransition, M as MapView, a as MapViewChangeEvent } from './contract-BaK24QZD.js';
6
6
  import '@maplibre/maplibre-react-native';
7
7
  import 'react-native';
8
8
  import '@tileflow/core/native';