@assetlib/sdk-core 0.7.5-preview.1 → 0.7.7-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 +3 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +36 -6
- package/dist/telemetry.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ App-owned catalog placements may include `bundledAccessibility` with the same sh
|
|
|
86
86
|
|
|
87
87
|
## Image formats and sizes
|
|
88
88
|
|
|
89
|
-
Preview 0.2 accepts the optional signed `renditionSchemaVersion: 1` extension. Existing schema-1 manifests still work; older clients can use the mandatory WebP fallback in new manifests. By default, the client selects PNG or WebP. Ask for physical pixels with both `pixelWidth` and `pixelHeight`; without a target, selection uses the generated
|
|
89
|
+
Preview 0.2 accepts the optional signed `renditionSchemaVersion: 1` extension. Existing schema-1 manifests still work; older clients can use the mandatory WebP fallback in new manifests. By default, the client selects PNG or WebP. Ask for physical pixels with both `pixelWidth` and `pixelHeight`; without a target, selection uses the generated reference dimensions. Core cannot see layout: the Expo components pass the measured view × display scale (see the [Expo README](../sdk-expo/README.md#sizing)); other callers pass their own target.
|
|
90
90
|
|
|
91
91
|
Selection uses the smallest rendition covering both dimensions, then byte size and hash. If every rendition is smaller, the largest is preferred. Failed candidates advance to the next candidate, then the legacy fallback. Historical releases remain cache-only. The result describes the actual selected MIME, hash and pixel dimensions. It does not convert one format into another on the device.
|
|
92
92
|
|
|
@@ -102,7 +102,7 @@ A catalog placement can declare `"rendering": "template"`. Assetlib then deliver
|
|
|
102
102
|
|
|
103
103
|
Signed image descriptors (placements, state members, variant cells and catalog page images) may carry `rendering`. A present value that is not a string matching `^[a-z][a-z0-9-]{0,31}$` rejects the release; absent means `original`. Resolution compares the reference's rendering with the descriptor actually selected after arm, appearance and state selection. If they differ, or the descriptor names a value this client does not know, that release's descriptor is skipped as for an incompatible size: its cache entry is not read and nothing is downloaded. When the current release publishes the key, resolution continues with the usual order, a compatible verified cached image from an older retained release and then the bundled fallback (`fallbackReason: 'missing'`). Other placements are unaffected. Catalog page images resolve as `original`. `ResolvedAsset.rendering` reports the delivered descriptor's rendering, `'original'` or `'template'`.
|
|
104
104
|
|
|
105
|
-
The bundled fallback must also be a single-color shape, such as a PNG with alpha, because the app applies the same tint to remote and bundled artwork. Remote masks are rasters, so request physical pixels with `pixelWidth` and `pixelHeight` (
|
|
105
|
+
The bundled fallback must also be a single-color shape, such as a PNG with alpha, because the app applies the same tint to remote and bundled artwork. Remote masks are rasters, so request physical pixels with `pixelWidth` and `pixelHeight` (view size × display scale); the Expo components do this for every placement. Template rendering needs this SDK and CLI or newer: older code generation drops the field, and older runtimes do not check it. Publishing template descriptors from the hosted console is planned; until then releases carry no `rendering` field.
|
|
106
106
|
|
|
107
107
|
## Offline typed references
|
|
108
108
|
|
|
@@ -121,7 +121,7 @@ Check a catalog into your application:
|
|
|
121
121
|
node node_modules/@assetlib/sdk-core/bin/codegen.mjs assetlib.catalog.json > src/assets.generated.ts
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
Use `AppAssets.Travel.coast` in application code. Existing compatible artwork updates do not require regenerating this catalog. Adding a new application placement does. Schema v1 identifies placements by key and by width and height.
|
|
124
|
+
Use `AppAssets.Travel.coast` in application code. Existing compatible artwork updates do not require regenerating this catalog. Adding a new application placement does. Schema v1 identifies placements by key and by width and height. Width and height are a reference size: they fix the aspect ratio and identify the placement; the SDK sizes requests from the view. Every published image must match that aspect ratio (the console checks it when an image is assigned and again at publish) and may have any pixel size. `resolve` without `pixelWidth` and `pixelHeight` uses the reference size as its target; the Expo components measure the view instead. Changing the numbers changes the placement's identity, so never renumber a catalog to change sharpness. Stable placement IDs and separately versioned contracts are not implemented yet. Renaming a key requires a coordinated migration, not simply renaming it in the console.
|
|
125
125
|
|
|
126
126
|
## Appearance and arm variants
|
|
127
127
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -224,18 +224,35 @@ async function fetchBounded(fetcher, url, maxBytes, timeoutMs, accept = 'image/w
|
|
|
224
224
|
controller.abort();
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
|
+
/** Iterative, so any depth JSON.parse accepted freezes without exhausting the stack. */
|
|
228
|
+
function deepFreeze(value) {
|
|
229
|
+
const pending = [value];
|
|
230
|
+
while (pending.length) {
|
|
231
|
+
const item = pending.pop();
|
|
232
|
+
if (item === null || typeof item !== 'object')
|
|
233
|
+
continue;
|
|
234
|
+
Object.freeze(item);
|
|
235
|
+
for (const child of Object.values(item))
|
|
236
|
+
pending.push(child);
|
|
237
|
+
}
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
227
240
|
/** Durable replay identity survives authorized route and signing-key changes. */
|
|
228
241
|
export function storageNamespace(config) {
|
|
229
242
|
return hashBytes(utf8ToBytes(JSON.stringify([new URL(config.manifestUrl).origin, config.orgId, config.appId, config.environment]))).slice(0, 32);
|
|
230
243
|
}
|
|
231
244
|
/** Use the same verification for normal state loads and legacy namespace migration. */
|
|
232
245
|
export function verifyStoredState(serialized, config) {
|
|
246
|
+
return verifyStoredHistory(serialized, config).state;
|
|
247
|
+
}
|
|
248
|
+
function verifyStoredHistory(serialized, config) {
|
|
233
249
|
if (utf8ToBytes(serialized).length > SDK_LIMITS.stateBytes)
|
|
234
250
|
throw new Error('Stored SDK state exceeds its bound.');
|
|
235
251
|
const state = JSON.parse(serialized);
|
|
236
252
|
if (!record(state) || state.version !== 1 || !integer(state.highestSequence, 1, 2_147_483_647) || !Array.isArray(state.history) || state.history.length < 1 || state.history.length > SDK_LIMITS.retainedReleases)
|
|
237
253
|
throw new Error('Stored SDK state is invalid.');
|
|
238
254
|
let previous = state.highestSequence + 1;
|
|
255
|
+
const payloads = [];
|
|
239
256
|
for (const [index, entry] of state.history.entries()) {
|
|
240
257
|
const { payload } = verifySignedManifest(entry, config);
|
|
241
258
|
if (payload.sequence >= previous)
|
|
@@ -243,13 +260,16 @@ export function verifyStoredState(serialized, config) {
|
|
|
243
260
|
if (index === 0 && payload.sequence !== state.highestSequence)
|
|
244
261
|
throw new Error('Stored sequence does not match its signed manifest.');
|
|
245
262
|
previous = payload.sequence;
|
|
263
|
+
payloads.push(payload);
|
|
246
264
|
}
|
|
247
|
-
return state;
|
|
265
|
+
return { state: state, payloads };
|
|
248
266
|
}
|
|
249
267
|
export class AssetClient {
|
|
250
268
|
options;
|
|
251
269
|
config;
|
|
252
270
|
state = { version: 1, highestSequence: 0, history: [] };
|
|
271
|
+
adoptedRaw = null;
|
|
272
|
+
payloads = [];
|
|
253
273
|
initialized = false;
|
|
254
274
|
storageFailure = null;
|
|
255
275
|
lastError = null;
|
|
@@ -322,16 +342,22 @@ export class AssetClient {
|
|
|
322
342
|
throw new Error('Stored release state disappeared.');
|
|
323
343
|
return;
|
|
324
344
|
}
|
|
325
|
-
|
|
345
|
+
// These exact bytes were already verified with this client's fixed config.
|
|
346
|
+
if (raw === this.adoptedRaw)
|
|
347
|
+
return;
|
|
348
|
+
const { state: next, payloads } = verifyStoredHistory(raw, this.config);
|
|
326
349
|
if (next.highestSequence < this.state.highestSequence)
|
|
327
350
|
throw new Error('Stored release state moved backwards.');
|
|
328
|
-
const accepted = new Map(this.state.history.map(entry => [
|
|
329
|
-
for (const entry of next.history) {
|
|
330
|
-
const previous = accepted.get(
|
|
351
|
+
const accepted = new Map(this.state.history.map((entry, index) => [this.payloads[index].sequence, entry.payload]));
|
|
352
|
+
for (const [index, entry] of next.history.entries()) {
|
|
353
|
+
const previous = accepted.get(payloads[index].sequence);
|
|
331
354
|
if (previous !== undefined && previous !== entry.payload)
|
|
332
355
|
throw new Error('Stored release state conflicts with an accepted release.');
|
|
333
356
|
}
|
|
357
|
+
const frozen = payloads.map(deepFreeze);
|
|
334
358
|
this.state = next;
|
|
359
|
+
this.payloads = frozen;
|
|
360
|
+
this.adoptedRaw = raw;
|
|
335
361
|
}
|
|
336
362
|
catch {
|
|
337
363
|
this.storageFailure = 'Stored release state could not be verified. Using bundled assets; repair or explicitly reset app data before reconnecting.';
|
|
@@ -367,8 +393,12 @@ export class AssetClient {
|
|
|
367
393
|
const serialized = JSON.stringify(next);
|
|
368
394
|
if (utf8ToBytes(serialized).length > SDK_LIMITS.stateBytes)
|
|
369
395
|
throw new Error('Release history exceeds the storage bound.');
|
|
396
|
+
const payloads = [deepFreeze(payload), ...this.payloads].slice(0, SDK_LIMITS.retainedReleases);
|
|
370
397
|
await this.options.storage.saveState(serialized);
|
|
398
|
+
// Nothing may throw between these: state, payloads and adoptedRaw describe one history.
|
|
371
399
|
this.state = next;
|
|
400
|
+
this.payloads = payloads;
|
|
401
|
+
this.adoptedRaw = serialized;
|
|
372
402
|
this.lastError = null;
|
|
373
403
|
return { updated: true, sequence: payload.sequence };
|
|
374
404
|
}
|
|
@@ -382,7 +412,7 @@ export class AssetClient {
|
|
|
382
412
|
}
|
|
383
413
|
/** Only metadata transitions use the serial queue. Image transfers are bounded and concurrent. */
|
|
384
414
|
snapshot() {
|
|
385
|
-
return this.serial(async () => { await this.load(); return this.storageFailure ? [] : this.
|
|
415
|
+
return this.serial(async () => { await this.load(); return this.storageFailure ? [] : this.payloads.map(payload => ({ payload })); });
|
|
386
416
|
}
|
|
387
417
|
withTransfer(work, signal) {
|
|
388
418
|
throwIfAborted(signal);
|
package/dist/telemetry.js
CHANGED
|
@@ -28,7 +28,7 @@ export class ObservationReporter {
|
|
|
28
28
|
this.fetcher = fetcher;
|
|
29
29
|
this.interval = boundedInteger(options.flushIntervalMs, 60_000, 2_147_483_647);
|
|
30
30
|
this.maxBatch = boundedInteger(options.maxBatch, 200, 200);
|
|
31
|
-
const sdk = options.sdk ?? { name: 'sdk-core', version: '0.7.
|
|
31
|
+
const sdk = options.sdk ?? { name: 'sdk-core', version: '0.7.7-preview.1' };
|
|
32
32
|
const build = options.build ?? { platform: 'web', appVersion: 'unknown', buildNumber: 'unknown' };
|
|
33
33
|
if (!version(sdk.name) || !version(sdk.version) || !['ios', 'android', 'web', 'expo'].includes(build.platform) || !version(build.appVersion) || !version(build.buildNumber) || (options.installId !== undefined && !validId(options.installId)))
|
|
34
34
|
throw new Error('Invalid observation configuration.');
|