@assetlib/sdk-expo 0.7.7-preview.1 → 0.7.8-preview.1
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 +2 -2
- package/package.json +2 -2
- package/src/index.tsx +18 -13
- package/src/telemetry.ts +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Create a long-lived client outside repeated renders. After an explicit `refresh(
|
|
|
36
36
|
|
|
37
37
|
### Sizing
|
|
38
38
|
|
|
39
|
-
A placement's width and height are a reference size: they fix the aspect ratio and identify the placement; the SDK sizes requests from the view. Without `pixelWidth` and `pixelHeight`, `AssetlibImage` and `AssetlibStateImage` request the view's measured layout (`onLayout`) × `PixelRatio.get()`, enough to cover the view (to fit it for `contentFit` `contain` or `scale-down`), rounded up to the console's rendition tiers: 512, 1024 or 2048 pixels on the long edge, or 24, 48, 72, 96, 192 or 512 for tintable icons. The request asks for 98% of the tier, so a rendition a few pixels short of its tier still serves it, and an unusual aspect ratio whose rendition falls further short gets the next larger one. A new view starts from the largest tier this client has measured for the placement,
|
|
39
|
+
A placement's width and height are a reference size: they fix the aspect ratio and identify the placement; the SDK sizes requests from the view. Without `pixelWidth` and `pixelHeight`, `AssetlibImage` and `AssetlibStateImage` request the view's measured layout (`onLayout`) × `PixelRatio.get()`, enough to cover the view (to fit it for `contentFit` `contain` or `scale-down`), rounded up to the console's rendition tiers: 512, 1024 or 2048 pixels on the long edge, or 24, 48, 72, 96, 192 or 512 for tintable icons. The request asks for 98% of the tier, so a rendition a few pixels short of its tier still serves it, and an unusual aspect ratio whose rendition falls further short gets the next larger one. A new view starts from the largest tier this client has measured for the placement. Without one, it draws its bundled fallback and waits for its first layout before it resolves, so a view whose first layout has a size resolves once. A first layout without a size resolves at the reference size × `PixelRatio.get()`, and the view's first sized layout then replaces that estimate. Once a measured layout has set a view's tier, it only grows, so rotating or resizing for split view never requests a smaller image; growing into a larger tier starts a new resolution, which shows the bundled fallback until the larger image is ready. Pass both `pixelWidth` and `pixelHeight` to request exact pixels instead. `AssetlibDynamicImage` keeps requesting its reference's own size unless you pass them.
|
|
40
40
|
|
|
41
41
|
`onStatus` reports source selection and verification, including the effective `arm` (`null` for control) and `armSource` (`explicit`, `decision`, `control`, or `invalid-decision`). Initial bundle and renderer-failure statuses use `arm: null` and `armSource: 'control'`; resolved statuses preserve the core's decision diagnostics. This callback is not a user impression or proof that the image was visible. Do not send experiment exposure events from this callback. App layout, accessibility labels, and refresh policy remain app-owned. Rendering uses `expo-image` for downloaded PNG/WebP support on iOS and Android; props follow Expo Image except `cachePolicy`, which controls Assetlib's verified byte retention. The renderer's independent cache is always disabled. Pending resolution is cancelled when a component unmounts or changes its asset request.
|
|
42
42
|
|
|
@@ -239,7 +239,7 @@ await client.flush();
|
|
|
239
239
|
client.dispose(); // Remove the AppState listener and stop observation timers.
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
The adapter identifies itself as `sdk-expo` version `0.7.
|
|
242
|
+
The adapter identifies itself as `sdk-expo` version `0.7.8-preview.1`. An explicit `telemetry.sdk` or `telemetry.build` takes precedence. When the runtime can optionally require `expo-application` or `expo-constants`, the adapter reads native app version and build number (falling back to Expo config version/build). Missing packages, unavailable runtime loaders, and throwing native bridges are ignored; no new dependency is required. Supply `build` explicitly for reliable production attribution, especially with bundlers that cannot resolve optional runtime requires or in Expo Go. Without complete runtime metadata or an explicit build, batches use platform `expo` and version/build `unknown`.
|
|
243
243
|
|
|
244
244
|
Only the observation contract fields are sent: environment, SDK name/version, build platform/version/number, random install ID, send time, and coalesced events with placement key, kind/count, source, asset ID/release sequence when known, effective arm/appearance when selected, and fallback reason. Bundle outcomes send only `fallback` events without asset ID or sequence. Dynamic catalog references do not emit observations because they have no placement key. Image bytes, descriptions, URLs, device identifiers, account identifiers, and status/error messages are not included.
|
|
245
245
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assetlib/sdk-expo",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8-preview.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.tsx",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"test": "node --test test/*.test.mjs"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@assetlib/sdk-core": "0.7.
|
|
23
|
+
"@assetlib/sdk-core": "0.7.8-preview.1"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"expo": ">=54 <58",
|
package/src/index.tsx
CHANGED
|
@@ -49,7 +49,7 @@ const ARTWORK_TIERS = [512, 1024, 2048];
|
|
|
49
49
|
const ICON_TIERS = [24, 48, 72, 96, 192, 512];
|
|
50
50
|
/** The largest tier measured per placement in this client's session; it seeds new mounts. */
|
|
51
51
|
const sessionTiers = new WeakMap<AssetClient, Map<string, number>>();
|
|
52
|
-
type MeasuredTier = { client: AssetClient; id: string; tier: number; measured: boolean };
|
|
52
|
+
type MeasuredTier = { client: AssetClient; id: string; tier: number; measured: boolean; laidOut: boolean };
|
|
53
53
|
|
|
54
54
|
const tiersFor = (asset: AssetRef) => asset.rendering === 'template' ? ICON_TIERS : ARTWORK_TIERS;
|
|
55
55
|
function tierFor(asset: AssetRef, longEdge: number): number {
|
|
@@ -82,8 +82,9 @@ function measuredTier(asset: AssetRef, layout: { width: number; height: number }
|
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
84
|
* Without an explicit target, request the measured view × PixelRatio snapped to a tier. A mount starts
|
|
85
|
-
* from the session's tier for this placement
|
|
86
|
-
*
|
|
85
|
+
* from the session's tier for this placement; without one it is pending until its first layout, so it
|
|
86
|
+
* resolves once. A first layout without a size uses the reference size × PixelRatio until a sized layout
|
|
87
|
+
* replaces it. Once measured, only this view's own layout changes its tier, and only upward.
|
|
87
88
|
*/
|
|
88
89
|
function usePixelTarget(client: AssetClient, asset: AssetRef, pixelWidth: number | undefined, pixelHeight: number | undefined, contentFit: ImageProps['contentFit'], onLayout: ImageProps['onLayout']) {
|
|
89
90
|
const explicit = pixelWidth !== undefined || pixelHeight !== undefined;
|
|
@@ -92,21 +93,23 @@ function usePixelTarget(client: AssetClient, asset: AssetRef, pixelWidth: number
|
|
|
92
93
|
sessionTiers.set(client, tiers);
|
|
93
94
|
const seed = (): MeasuredTier => {
|
|
94
95
|
const known = tiers.get(id);
|
|
95
|
-
return { client, id, measured: known !== undefined, tier: known ?? tierFor(asset, Math.max(asset.width, asset.height) * displayScale()) };
|
|
96
|
+
return { client, id, measured: known !== undefined, laidOut: false, tier: known ?? tierFor(asset, Math.max(asset.width, asset.height) * displayScale()) };
|
|
96
97
|
};
|
|
97
98
|
const [stored, setStored] = useState(seed);
|
|
98
99
|
let current = stored;
|
|
99
100
|
if (stored.client !== client || stored.id !== id) setStored(current = seed());
|
|
100
101
|
return {
|
|
101
102
|
...(explicit ? { pixelWidth, pixelHeight } : tierTarget(asset, current.tier)),
|
|
103
|
+
pending: !explicit && !current.measured && !current.laidOut,
|
|
102
104
|
onLayout(event: Parameters<NonNullable<ImageProps['onLayout']>>[0]) {
|
|
103
105
|
const measured = explicit ? undefined : measuredTier(asset, event.nativeEvent.layout, displayScale(), contentFit);
|
|
104
|
-
if (measured !== undefined)
|
|
105
|
-
|
|
106
|
+
if (measured !== undefined) tiers.set(id, Math.max(tiers.get(id) ?? measured, measured));
|
|
107
|
+
if (!explicit) {
|
|
106
108
|
setStored(previous => {
|
|
107
109
|
const base = previous.client === client && previous.id === id ? previous : current;
|
|
110
|
+
if (measured === undefined) return base.measured || base.laidOut ? base : { ...base, laidOut: true };
|
|
108
111
|
const tier = base.measured ? Math.max(base.tier, measured) : measured;
|
|
109
|
-
return base.measured && tier === base.tier ? base : { client, id, measured: true, tier };
|
|
112
|
+
return base.measured && tier === base.tier ? base : { client, id, measured: true, laidOut: true, tier };
|
|
110
113
|
});
|
|
111
114
|
}
|
|
112
115
|
onLayout?.(event);
|
|
@@ -147,7 +150,7 @@ function matchesLoadedSource(event: Parameters<NonNullable<ImageProps['onLoad']>
|
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
/** A request token hides the previous asset immediately when a recycled view changes identity. */
|
|
150
|
-
function useResolvedImage(client: AssetClient, asset: AssetRef | DynamicAssetRef, request: object, resolve: (signal: AbortSignal) => Promise<ResolvedAsset>, fallback: ImageSource, fallbackAccessibility: AssetAccessibility | undefined, requireDescription: boolean, onStatus?: (status: AssetStatus) => void) {
|
|
153
|
+
function useResolvedImage(client: AssetClient, asset: AssetRef | DynamicAssetRef, request: object, resolve: (signal: AbortSignal) => Promise<ResolvedAsset>, fallback: ImageSource, fallbackAccessibility: AssetAccessibility | undefined, requireDescription: boolean, onStatus?: (status: AssetStatus) => void, pending = false) {
|
|
151
154
|
const [resolved, setResolved] = useState<{ request: object; image: ImageUri; result: ResolvedAsset; accessibility?: AssetAccessibility } | null>(null);
|
|
152
155
|
const callbacks = useRef({ resolve, onStatus });
|
|
153
156
|
callbacks.current = { resolve, onStatus };
|
|
@@ -156,6 +159,7 @@ function useResolvedImage(client: AssetClient, asset: AssetRef | DynamicAssetRef
|
|
|
156
159
|
currentRequest.current = request;
|
|
157
160
|
const displayed = useRef(false);
|
|
158
161
|
useEffect(() => {
|
|
162
|
+
if (pending) { setResolved(null); return; }
|
|
159
163
|
const controller = new AbortController();
|
|
160
164
|
let localUri: ImageUri | null = null;
|
|
161
165
|
setResolved(null);
|
|
@@ -183,7 +187,7 @@ function useResolvedImage(client: AssetClient, asset: AssetRef | DynamicAssetRef
|
|
|
183
187
|
localUri?.release();
|
|
184
188
|
if (currentUri.current === localUri) currentUri.current = null;
|
|
185
189
|
};
|
|
186
|
-
}, [client, request]);
|
|
190
|
+
}, [client, request, pending]);
|
|
187
191
|
const isCurrent = () => resolved?.request === request && currentRequest.current === request && currentUri.current === resolved.image;
|
|
188
192
|
const selected = resolved?.request === request ? resolved : null;
|
|
189
193
|
const rendererKey = useRendererKey(request, selected?.image ?? fallback);
|
|
@@ -209,11 +213,11 @@ function useResolvedImage(client: AssetClient, asset: AssetRef | DynamicAssetRef
|
|
|
209
213
|
export function AssetlibImage<R extends AssetRef>({ client, asset, arm, fallback, fallbackDark, appearance = 'system', fallbackAccessibility = asset.bundledAccessibility, accessibilityMode, accessibilityLocale, revision = 0, pixelWidth: requestedPixelWidth, pixelHeight: requestedPixelHeight, cachePolicy, onStatus, onError, onLoad, ...rest }: AssetlibImageProps<R>) {
|
|
210
214
|
const props: Omit<ImageProps, 'source' | 'cachePolicy'> = rest;
|
|
211
215
|
const effectiveAppearance = useEffectiveAppearance(appearance);
|
|
212
|
-
const { pixelWidth, pixelHeight, onLayout } = usePixelTarget(client, asset, requestedPixelWidth, requestedPixelHeight, props.contentFit, props.onLayout);
|
|
216
|
+
const { pixelWidth, pixelHeight, pending, onLayout } = usePixelTarget(client, asset, requestedPixelWidth, requestedPixelHeight, props.contentFit, props.onLayout);
|
|
213
217
|
useTemplateTintWarning(asset, props.tintColor);
|
|
214
218
|
const requireDescription = accessibilityMode === 'description' && props.accessibilityLabel == null;
|
|
215
219
|
const request = useMemo(() => ({}), [client, asset.key, asset.width, asset.height, asset.rendering, arm, revision, pixelWidth, pixelHeight, cachePolicy, requireDescription, effectiveAppearance]);
|
|
216
|
-
const image = useResolvedImage(client, asset, request, signal => client.resolve(asset, { pixelWidth, pixelHeight, cachePolicy, appearance: effectiveAppearance, arm, signal }), effectiveAppearance === 'dark' ? fallbackDark ?? fallback : fallback, fallbackAccessibility, requireDescription, onStatus);
|
|
220
|
+
const image = useResolvedImage(client, asset, request, signal => client.resolve(asset, { pixelWidth, pixelHeight, cachePolicy, appearance: effectiveAppearance, arm, signal }), effectiveAppearance === 'dark' ? fallbackDark ?? fallback : fallback, fallbackAccessibility, requireDescription, onStatus, pending);
|
|
217
221
|
return <Image {...props} key={image.rendererKey} {...accessibilityProps(props, accessibilityMode, image.accessibility, accessibilityLocale)} cachePolicy="none" source={image.source} onLayout={onLayout} onLoad={event => { image.loaded(event); onLoad?.(event); }} onError={event => { image.fail(); onError?.(event); }} />;
|
|
218
222
|
}
|
|
219
223
|
|
|
@@ -230,7 +234,7 @@ export function AssetlibDynamicImage({ client, asset, fallback, fallbackDark, ap
|
|
|
230
234
|
export function AssetlibStateImage<R extends StateSetRef>({ client, asset, arm, state, fallbacks, fallbacksDark, appearance = 'system', fallbackAccessibility = asset.bundledStateAccessibility, accessibilityMode, accessibilityLocale, revision = 0, pixelWidth: requestedPixelWidth, pixelHeight: requestedPixelHeight, cachePolicy, onStatus, onError, onLoad, ...rest }: AssetlibStateImageProps<R>) {
|
|
231
235
|
const props: Omit<ImageProps, 'source' | 'cachePolicy'> = rest;
|
|
232
236
|
const effectiveAppearance = useEffectiveAppearance(appearance);
|
|
233
|
-
const { pixelWidth, pixelHeight, onLayout } = usePixelTarget(client, asset, requestedPixelWidth, requestedPixelHeight, props.contentFit, props.onLayout);
|
|
237
|
+
const { pixelWidth, pixelHeight, pending, onLayout } = usePixelTarget(client, asset, requestedPixelWidth, requestedPixelHeight, props.contentFit, props.onLayout);
|
|
234
238
|
useTemplateTintWarning(asset, props.tintColor);
|
|
235
239
|
const statesKey = JSON.stringify(asset.states);
|
|
236
240
|
const requireDescription = accessibilityMode === 'description' && props.accessibilityLabel == null;
|
|
@@ -246,6 +250,7 @@ export function AssetlibStateImage<R extends StateSetRef>({ client, asset, arm,
|
|
|
246
250
|
if (asset.states.some(name => !Object.prototype.hasOwnProperty.call(fallbacks, name) || fallbacks[name] == null)) throw new Error('Every artwork state requires its own bundled fallback.');
|
|
247
251
|
if (fallbacksDark && asset.states.some(name => !Object.prototype.hasOwnProperty.call(fallbacksDark, name) || fallbacksDark[name] == null)) throw new Error('Every artwork state requires its own dark bundled fallback when fallbacksDark is supplied.');
|
|
248
252
|
useEffect(() => {
|
|
253
|
+
if (pending) { setResolved(null); return; }
|
|
249
254
|
const controller = new AbortController();
|
|
250
255
|
const images: Record<string, ImageUri> = Object.create(null);
|
|
251
256
|
const accessibility: Record<string, AssetAccessibility | undefined> = Object.create(null);
|
|
@@ -282,7 +287,7 @@ export function AssetlibStateImage<R extends StateSetRef>({ client, asset, arm,
|
|
|
282
287
|
controller.abort(); release();
|
|
283
288
|
if (currentUris.current === images) currentUris.current = null;
|
|
284
289
|
};
|
|
285
|
-
}, [client, request]);
|
|
290
|
+
}, [client, request, pending]);
|
|
286
291
|
const selected = resolved?.request === request ? resolved.images[state] : null;
|
|
287
292
|
const metadata = selected ? resolved!.accessibility[state] : fallbackAccessibility?.[state];
|
|
288
293
|
const fallback = (effectiveAppearance === 'dark' ? fallbacksDark ?? fallbacks : fallbacks)[state];
|
package/src/telemetry.ts
CHANGED
|
@@ -48,7 +48,7 @@ export function expoTelemetry(telemetry: ClientOptions['telemetry']): ClientOpti
|
|
|
48
48
|
if (!telemetry?.enabled) return telemetry;
|
|
49
49
|
return {
|
|
50
50
|
...telemetry,
|
|
51
|
-
sdk: telemetry.sdk ?? { name: 'sdk-expo', version: '0.7.
|
|
51
|
+
sdk: telemetry.sdk ?? { name: 'sdk-expo', version: '0.7.8-preview.1' },
|
|
52
52
|
build: telemetry.build ?? runtimeBuild() ?? { platform: 'expo', appVersion: 'unknown', buildNumber: 'unknown' },
|
|
53
53
|
};
|
|
54
54
|
}
|