@assetlib/sdk-core 0.7.10-preview.1 → 0.7.11-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 +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +29 -5
- package/dist/telemetry.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ const asset = await client.resolve(AppAssets.Travel.coast, { pixelWidth: 600, pi
|
|
|
20
20
|
// source is 'remote', 'cache', or 'bundle'. Render a bundled image for 'bundle'.
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`refresh()` returns `{ updated, sequence, error? }`. Errors preserve the previous accepted release. `resolve()` returns `{ source, sequence, message, arm, armSource, bytes?, sha256?, assetId?, mime?, pixelWidth?, pixelHeight?, rendering? }`. `getStatus()` returns `{ initialized, sequence, lastError }`. Use one long-lived client per configured app.
|
|
23
|
+
`refresh()` returns `{ updated, sequence, error? }`. Errors preserve the previous accepted release. After a release is accepted, later refreshes send the service's `ETag` as `If-None-Match`, and a `304 Not Modified` returns `{ updated: false, sequence }` without downloading or re-verifying the manifest; the validator lives in client memory and is only sent while durable state still holds that exact release. `resolve()` returns `{ source, sequence, message, arm, armSource, bytes?, sha256?, assetId?, mime?, pixelWidth?, pixelHeight?, rendering? }`. `getStatus()` returns `{ initialized, sequence, lastError }`. Use one long-lived client per configured app.
|
|
24
24
|
|
|
25
25
|
Public config: `{ schemaVersion: 1, orgId, appId, environment: 'staging' | 'production', manifestUrl, pinnedPublicKey?, keyId?, pinnedPublicKeys?, keyIds? }`. Supply `pinnedPublicKey` or a `pinnedPublicKeys` array of 1–16 distinct exact-PEM Ed25519 SPKI public keys. Each PEM is limited to 256 UTF-8 bytes. When both are present, the single key must exactly match a member of the set. Configurations with only `pinnedPublicKey` retain their existing behavior and parsed shape.
|
|
26
26
|
|
package/dist/index.d.ts
CHANGED
|
@@ -48,6 +48,9 @@ export declare class AssetClient {
|
|
|
48
48
|
private initialized;
|
|
49
49
|
private storageFailure;
|
|
50
50
|
private lastError;
|
|
51
|
+
/** The ETag the delivery service gave the envelope this client last accepted, so a later refresh can ask for it
|
|
52
|
+
conditionally. Held in memory only and sent only while the durable state still holds that exact release. */
|
|
53
|
+
private manifestEtag;
|
|
51
54
|
private lastRefreshReason;
|
|
52
55
|
private reporter?;
|
|
53
56
|
private queue;
|
package/dist/index.js
CHANGED
|
@@ -161,7 +161,14 @@ class DeliveryError extends Error {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
const failureReason = (error) => error instanceof DeliveryError ? error.reason : 'other';
|
|
164
|
+
/** An ETag worth sending back: a quoted (optionally weak) validator of bounded length. Anything else is ignored. */
|
|
165
|
+
const usableEtag = (value) => value && /^(?:W\/)?"[^"]{1,200}"$/.test(value) ? value : null;
|
|
164
166
|
async function fetchBounded(fetcher, url, maxBytes, timeoutMs, accept = 'image/webp', signal, noStore = false) {
|
|
167
|
+
return (await fetchBoundedResponse(fetcher, url, maxBytes, timeoutMs, accept, signal, noStore)).bytes;
|
|
168
|
+
}
|
|
169
|
+
/** Like fetchBounded, but with `ifNoneMatch` set the request is conditional and a 304 returns `notModified` without a body.
|
|
170
|
+
A 304 to an unconditional request is a server error and fails like any other non-2xx status. */
|
|
171
|
+
async function fetchBoundedResponse(fetcher, url, maxBytes, timeoutMs, accept = 'image/webp', signal, noStore = false, ifNoneMatch) {
|
|
165
172
|
throwIfAborted(signal);
|
|
166
173
|
const controller = new AbortController();
|
|
167
174
|
let timeout;
|
|
@@ -171,11 +178,15 @@ async function fetchBounded(fetcher, url, maxBytes, timeoutMs, accept = 'image/w
|
|
|
171
178
|
const request = async () => {
|
|
172
179
|
let response;
|
|
173
180
|
try {
|
|
174
|
-
response = await fetcher(url, { method: 'GET', credentials: 'omit', redirect: 'error', ...(noStore ? { cache: 'no-store' } : {}), signal: controller.signal, headers: { Accept: url.endsWith('/manifest') ? 'application/json' : accept } });
|
|
181
|
+
response = await fetcher(url, { method: 'GET', credentials: 'omit', redirect: 'error', ...(noStore ? { cache: 'no-store' } : {}), signal: controller.signal, headers: { Accept: url.endsWith('/manifest') ? 'application/json' : accept, ...(ifNoneMatch ? { 'If-None-Match': ifNoneMatch } : {}) } });
|
|
175
182
|
}
|
|
176
183
|
catch (error) {
|
|
177
184
|
throw new DeliveryError(error instanceof Error ? error.message : 'Assetlib delivery is unavailable.', 'offline');
|
|
178
185
|
}
|
|
186
|
+
if (ifNoneMatch && response.status === 304 && !response.redirected) {
|
|
187
|
+
await response.body?.cancel().catch(() => { });
|
|
188
|
+
return { bytes: new Uint8Array(0), etag: usableEtag(response.headers.get('etag')), notModified: true };
|
|
189
|
+
}
|
|
179
190
|
if (!response.ok || response.redirected)
|
|
180
191
|
throw new DeliveryError(`Assetlib delivery returned ${response.status}.`, response.redirected ? 'verification' : response.status === 404 ? 'missing' : 'other');
|
|
181
192
|
const declared = response.headers.get('content-length');
|
|
@@ -211,7 +222,7 @@ async function fetchBounded(fetcher, url, maxBytes, timeoutMs, accept = 'image/w
|
|
|
211
222
|
bytes.set(chunk, offset);
|
|
212
223
|
offset += chunk.byteLength;
|
|
213
224
|
}
|
|
214
|
-
return bytes;
|
|
225
|
+
return { bytes, etag: usableEtag(response.headers.get('etag')), notModified: false };
|
|
215
226
|
};
|
|
216
227
|
try {
|
|
217
228
|
return await Promise.race([request(), deadline, cancelled]);
|
|
@@ -273,6 +284,9 @@ export class AssetClient {
|
|
|
273
284
|
initialized = false;
|
|
274
285
|
storageFailure = null;
|
|
275
286
|
lastError = null;
|
|
287
|
+
/** The ETag the delivery service gave the envelope this client last accepted, so a later refresh can ask for it
|
|
288
|
+
conditionally. Held in memory only and sent only while the durable state still holds that exact release. */
|
|
289
|
+
manifestEtag = null;
|
|
276
290
|
lastRefreshReason = 'missing';
|
|
277
291
|
reporter;
|
|
278
292
|
queue = Promise.resolve();
|
|
@@ -372,20 +386,29 @@ export class AssetClient {
|
|
|
372
386
|
try {
|
|
373
387
|
if (this.storageFailure)
|
|
374
388
|
throw new DeliveryError(this.storageFailure, 'verification');
|
|
375
|
-
const
|
|
389
|
+
const current = this.state.history[0];
|
|
390
|
+
const known = this.manifestEtag && current && this.manifestEtag.sequence === this.state.highestSequence && this.manifestEtag.payload === current.payload ? this.manifestEtag : null;
|
|
391
|
+
const fetched = await fetchBoundedResponse(this.fetcher, this.config.manifestUrl, SDK_LIMITS.manifestBytes, this.timeoutMs, undefined, undefined, false, known?.etag);
|
|
392
|
+
if (fetched.notModified) {
|
|
393
|
+
// The service confirmed the accepted release is still current. Nothing was downloaded or re-verified.
|
|
394
|
+
this.lastError = null;
|
|
395
|
+
return { updated: false, sequence: this.state.highestSequence };
|
|
396
|
+
}
|
|
376
397
|
let verified;
|
|
377
398
|
try {
|
|
378
|
-
verified = verifySignedManifest(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)), this.config);
|
|
399
|
+
verified = verifySignedManifest(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(fetched.bytes)), this.config);
|
|
379
400
|
}
|
|
380
401
|
catch (error) {
|
|
381
402
|
throw new DeliveryError(error instanceof Error ? error.message : 'Manifest verification failed.', 'verification');
|
|
382
403
|
}
|
|
383
404
|
const { envelope, payload } = verified;
|
|
405
|
+
const remember = () => { this.manifestEtag = fetched.etag ? { etag: fetched.etag, sequence: payload.sequence, payload: envelope.payload } : null; };
|
|
384
406
|
if (payload.sequence < this.state.highestSequence)
|
|
385
407
|
throw new DeliveryError('An older release was rejected.', 'verification');
|
|
386
408
|
if (payload.sequence === this.state.highestSequence) {
|
|
387
|
-
if (envelope.payload !==
|
|
409
|
+
if (envelope.payload !== current?.payload)
|
|
388
410
|
throw new DeliveryError('Conflicting content reused an existing release sequence.', 'verification');
|
|
411
|
+
remember();
|
|
389
412
|
this.lastError = null;
|
|
390
413
|
return { updated: false, sequence: payload.sequence };
|
|
391
414
|
}
|
|
@@ -399,6 +422,7 @@ export class AssetClient {
|
|
|
399
422
|
this.state = next;
|
|
400
423
|
this.payloads = payloads;
|
|
401
424
|
this.adoptedRaw = serialized;
|
|
425
|
+
remember();
|
|
402
426
|
this.lastError = null;
|
|
403
427
|
return { updated: true, sequence: payload.sequence };
|
|
404
428
|
}
|
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.11-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.');
|