@absolutejs/absolute 0.20.0-beta.84 → 0.20.0-beta.86
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/angular/components/core/streamingSlotRegistrar.js +1 -1
- package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
- package/dist/build.js +370 -17
- package/dist/build.js.map +5 -5
- package/dist/cli/{compile-t73ac1zb.js → compile-e9cn3xpx.js} +4 -4
- package/dist/cli/{config-naw5fer4.js → config-z2kf085h.js} +1 -1
- package/dist/cli/{dev-skb8dz33.js → dev-3hweza3s.js} +2 -2
- package/dist/cli/{expoProject-3mmwtemn.js → expoProject-dac8fqhq.js} +1 -1
- package/dist/cli/{index-czw3jd8f.js → index-3tsmbyze.js} +22 -1
- package/dist/cli/{index-q78x1k9q.js → index-9rk7v35t.js} +2 -2
- package/dist/cli/{index-p8ezgd5e.js → index-tame6e2c.js} +65 -3
- package/dist/cli/{index-8gksmws3.js → index-x48brywr.js} +2 -2
- package/dist/cli/index.js +5 -5
- package/dist/cli/{mobile-pdck6a35.js → mobile-2p3z33t1.js} +85 -8
- package/dist/cli/{start-jbgfw27x.js → start-7t186mhg.js} +4 -4
- package/dist/index.js +370 -17
- package/dist/index.js.map +5 -5
- package/dist/mobile/index.js +689 -69
- package/dist/mobile/index.js.map +8 -8
- package/dist/mobile/remoteMacAgentEntry.js +237 -175
- package/dist/mobile/shellUpdate.js +415 -67
- package/dist/src/mobile/config.d.ts +5 -0
- package/dist/src/mobile/updateClient.d.ts +33 -0
- package/dist/src/mobile/updatePublisher.d.ts +12 -1
- package/dist/src/mobile/updateServer.d.ts +10 -0
- package/dist/types/build.d.ts +9 -0
- package/package.json +2 -2
|
@@ -157,17 +157,18 @@ var fileUrl = (manifestUrl, releaseId, path) => {
|
|
|
157
157
|
throw new TypeError("Mobile update asset escaped its signed release origin.");
|
|
158
158
|
return result;
|
|
159
159
|
};
|
|
160
|
-
var readChunks = async (reader, maximum, chunks = [], received = 0) => {
|
|
160
|
+
var readChunks = async (reader, maximum, onChunk, chunks = [], received = 0) => {
|
|
161
161
|
const result = await reader.read();
|
|
162
162
|
if (result.done)
|
|
163
163
|
return { chunks, received };
|
|
164
164
|
const total = received + result.value.byteLength;
|
|
165
165
|
if (total > maximum)
|
|
166
166
|
throw new TypeError("Mobile update response exceeds its signed size.");
|
|
167
|
+
await onChunk?.(result.value, received);
|
|
167
168
|
chunks.push(result.value);
|
|
168
|
-
return readChunks(reader, maximum, chunks, total);
|
|
169
|
+
return readChunks(reader, maximum, onChunk, chunks, total);
|
|
169
170
|
};
|
|
170
|
-
var readBounded = async (response, maximum) => {
|
|
171
|
+
var readBounded = async (response, maximum, onChunk) => {
|
|
171
172
|
const declared = Number(response.headers.get("content-length"));
|
|
172
173
|
if (Number.isFinite(declared) && declared > maximum)
|
|
173
174
|
throw new TypeError("Mobile update response exceeds its signed size.");
|
|
@@ -175,8 +176,9 @@ var readBounded = async (response, maximum) => {
|
|
|
175
176
|
return new Uint8Array;
|
|
176
177
|
const reader = response.body.getReader();
|
|
177
178
|
let result;
|
|
179
|
+
const chunks = [];
|
|
178
180
|
try {
|
|
179
|
-
result = await readChunks(reader, maximum);
|
|
181
|
+
result = await readChunks(reader, maximum, onChunk, chunks);
|
|
180
182
|
} catch (error) {
|
|
181
183
|
await reader.cancel().catch(() => {
|
|
182
184
|
return;
|
|
@@ -185,7 +187,7 @@ var readBounded = async (response, maximum) => {
|
|
|
185
187
|
}
|
|
186
188
|
const contents = new Uint8Array(result.received);
|
|
187
189
|
let offset = 0;
|
|
188
|
-
for (const chunk of
|
|
190
|
+
for (const chunk of chunks) {
|
|
189
191
|
contents.set(chunk, offset);
|
|
190
192
|
offset += chunk.byteLength;
|
|
191
193
|
}
|
|
@@ -198,6 +200,30 @@ var requestHeaders = (config) => ({
|
|
|
198
200
|
"x-absolute-mobile-release": config.currentReleaseId,
|
|
199
201
|
"x-absolute-mobile-runtime": config.runtimeFingerprint
|
|
200
202
|
});
|
|
203
|
+
var healthUrl = (manifestUrl) => new URL("./health", manifestUrl);
|
|
204
|
+
var reportAbsoluteMobileUpdateHealth = async (config, input, request = globalThis.fetch) => {
|
|
205
|
+
const manifestUrl = exactManifestUrl(config.manifestUrl);
|
|
206
|
+
const headers = new Headers(requestHeaders(config));
|
|
207
|
+
headers.set("content-type", "application/json");
|
|
208
|
+
headers.set("x-absolute-mobile-health-token", input.healthToken);
|
|
209
|
+
const response = await request(healthUrl(manifestUrl), {
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
kind: input.kind,
|
|
212
|
+
...input.reason ? { reason: input.reason } : {},
|
|
213
|
+
releaseId: input.releaseId,
|
|
214
|
+
...input.transfer ? { transfer: input.transfer } : {}
|
|
215
|
+
}),
|
|
216
|
+
cache: "no-store",
|
|
217
|
+
credentials: "omit",
|
|
218
|
+
headers,
|
|
219
|
+
keepalive: true,
|
|
220
|
+
method: "POST",
|
|
221
|
+
redirect: "error",
|
|
222
|
+
signal: AbortSignal.timeout(15000)
|
|
223
|
+
});
|
|
224
|
+
if (response.status !== 202)
|
|
225
|
+
throw new TypeError(`Mobile update health report failed with HTTP ${response.status}.`);
|
|
226
|
+
};
|
|
201
227
|
var requireCompatible = (manifest, config) => {
|
|
202
228
|
if (manifest.appId !== config.appId)
|
|
203
229
|
throw new TypeError("Mobile update belongs to another app.");
|
|
@@ -206,49 +232,172 @@ var requireCompatible = (manifest, config) => {
|
|
|
206
232
|
if (manifest.runtimeFingerprint !== config.runtimeFingerprint)
|
|
207
233
|
throw new TypeError("Mobile update requires a different native runtime.");
|
|
208
234
|
};
|
|
235
|
+
var networkConcurrency = (requested) => {
|
|
236
|
+
const bounded = Math.max(1, Math.min(6, Math.floor(requested ?? 3)));
|
|
237
|
+
const navigatorValue = Reflect.get(globalThis, "navigator");
|
|
238
|
+
const connection = typeof navigatorValue === "object" && navigatorValue !== null ? Reflect.get(navigatorValue, "connection") : undefined;
|
|
239
|
+
if (typeof connection !== "object" || connection === null)
|
|
240
|
+
return bounded;
|
|
241
|
+
if (Reflect.get(connection, "saveData") === true)
|
|
242
|
+
return 1;
|
|
243
|
+
const effectiveType = Reflect.get(connection, "effectiveType");
|
|
244
|
+
if (effectiveType === "slow-2g" || effectiveType === "2g")
|
|
245
|
+
return 1;
|
|
246
|
+
if (effectiveType === "3g")
|
|
247
|
+
return Math.min(2, bounded);
|
|
248
|
+
return bounded;
|
|
249
|
+
};
|
|
250
|
+
var combine = (prefix, suffix) => {
|
|
251
|
+
const result = new Uint8Array(prefix.byteLength + suffix.byteLength);
|
|
252
|
+
result.set(prefix);
|
|
253
|
+
result.set(suffix, prefix.byteLength);
|
|
254
|
+
return result;
|
|
255
|
+
};
|
|
256
|
+
var validContentRange = (value, start, total) => value === `bytes ${start}-${total - 1}/${total}`;
|
|
257
|
+
var combineSignals = (signals) => {
|
|
258
|
+
const nativeAny = Reflect.get(AbortSignal, "any");
|
|
259
|
+
if (typeof nativeAny === "function")
|
|
260
|
+
return Reflect.apply(nativeAny, AbortSignal, [signals]);
|
|
261
|
+
const controller = new AbortController;
|
|
262
|
+
const abort = () => controller.abort();
|
|
263
|
+
if (signals.some((signal) => signal.aborted))
|
|
264
|
+
abort();
|
|
265
|
+
else
|
|
266
|
+
signals.forEach((signal) => signal.addEventListener("abort", abort, { once: true }));
|
|
267
|
+
return controller.signal;
|
|
268
|
+
};
|
|
209
269
|
var createAbsoluteMobileUpdateClient = (options) => {
|
|
210
270
|
const manifestUrl = exactManifestUrl(options.config.manifestUrl);
|
|
211
271
|
const request = options.fetch ?? globalThis.fetch;
|
|
212
|
-
const downloadFiles = async (manifest
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
272
|
+
const downloadFiles = async (manifest) => {
|
|
273
|
+
const startedAt = performance.now();
|
|
274
|
+
const transfer = {
|
|
275
|
+
avoidedBytes: 0,
|
|
276
|
+
completedFiles: 0,
|
|
277
|
+
downloadedBytes: 0,
|
|
278
|
+
downloadedFiles: 0,
|
|
279
|
+
durationMs: 0,
|
|
280
|
+
resumedBytes: 0,
|
|
281
|
+
resumedFiles: 0,
|
|
282
|
+
reusedBytes: 0,
|
|
283
|
+
reusedFiles: 0,
|
|
284
|
+
throughputBytesPerSecond: 0,
|
|
285
|
+
totalBytes: manifest.files.reduce((total, file) => total + file.bytes, 0),
|
|
286
|
+
totalFiles: manifest.files.length
|
|
287
|
+
};
|
|
288
|
+
const updateTiming = () => {
|
|
289
|
+
transfer.durationMs = Math.max(0, performance.now() - startedAt);
|
|
290
|
+
transfer.avoidedBytes = transfer.reusedBytes + transfer.resumedBytes;
|
|
291
|
+
transfer.throughputBytesPerSecond = transfer.durationMs > 0 ? Math.round(transfer.downloadedBytes * 1000 / transfer.durationMs) : transfer.downloadedBytes;
|
|
292
|
+
};
|
|
293
|
+
const progress = () => {
|
|
294
|
+
updateTiming();
|
|
295
|
+
try {
|
|
296
|
+
options.onProgress?.({
|
|
297
|
+
...transfer,
|
|
298
|
+
kind: "download-progress"
|
|
299
|
+
});
|
|
300
|
+
} catch {}
|
|
301
|
+
};
|
|
302
|
+
const controller = new AbortController;
|
|
303
|
+
let next = 0;
|
|
304
|
+
let firstError;
|
|
305
|
+
const downloadFile = async (file) => {
|
|
306
|
+
const staged = await options.store.readStaged?.(file);
|
|
307
|
+
if (staged?.byteLength === file.bytes && await options.verifier.digest(staged) === file.sha256) {
|
|
308
|
+
transfer.resumedBytes += staged.byteLength;
|
|
309
|
+
transfer.resumedFiles += 1;
|
|
310
|
+
transfer.completedFiles += 1;
|
|
311
|
+
progress();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const reusable = await options.store.readReusable?.(file);
|
|
315
|
+
if (reusable?.byteLength === file.bytes && await options.verifier.digest(reusable) === file.sha256) {
|
|
316
|
+
await options.store.write(file, reusable);
|
|
317
|
+
transfer.reusedBytes += reusable.byteLength;
|
|
318
|
+
transfer.reusedFiles += 1;
|
|
319
|
+
transfer.completedFiles += 1;
|
|
320
|
+
progress();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const candidate = await options.store.readPartial?.(file);
|
|
324
|
+
if (candidate?.byteLength === file.bytes && await options.verifier.digest(candidate) === file.sha256) {
|
|
325
|
+
await options.store.write(file, candidate);
|
|
326
|
+
transfer.resumedBytes += candidate.byteLength;
|
|
327
|
+
transfer.resumedFiles += 1;
|
|
328
|
+
transfer.completedFiles += 1;
|
|
329
|
+
progress();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const partial = candidate && candidate.byteLength > 0 && candidate.byteLength < file.bytes ? candidate : new Uint8Array;
|
|
333
|
+
const headers = new Headers;
|
|
334
|
+
if (partial.byteLength > 0) {
|
|
335
|
+
headers.set("if-range", `"${file.sha256}"`);
|
|
336
|
+
headers.set("range", `bytes=${partial.byteLength}-`);
|
|
337
|
+
}
|
|
338
|
+
const asset = await request(fileUrl(manifestUrl, manifest.releaseId, file.path), {
|
|
339
|
+
cache: "no-store",
|
|
340
|
+
credentials: "omit",
|
|
341
|
+
headers,
|
|
342
|
+
redirect: "error",
|
|
343
|
+
signal: combineSignals([
|
|
344
|
+
controller.signal,
|
|
345
|
+
AbortSignal.timeout(30000)
|
|
346
|
+
])
|
|
230
347
|
});
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
348
|
+
if (!asset.ok)
|
|
349
|
+
throw new TypeError(`Mobile update asset ${file.path} failed with HTTP ${asset.status}.`);
|
|
350
|
+
const ranged = asset.status === 206;
|
|
351
|
+
if (ranged && (partial.byteLength === 0 || !validContentRange(asset.headers.get("content-range"), partial.byteLength, file.bytes)))
|
|
352
|
+
throw new TypeError(`Mobile update asset ${file.path} returned an invalid byte range.`);
|
|
353
|
+
const prefix = ranged ? partial : new Uint8Array;
|
|
354
|
+
if (ranged) {
|
|
355
|
+
transfer.resumedBytes += partial.byteLength;
|
|
356
|
+
transfer.resumedFiles += 1;
|
|
357
|
+
}
|
|
358
|
+
const downloaded = await readBounded(asset, file.bytes - prefix.byteLength, options.store.appendPartial ? async (chunk, offset) => {
|
|
359
|
+
await options.store.appendPartial?.(file, chunk, prefix.byteLength + offset);
|
|
360
|
+
transfer.downloadedBytes += chunk.byteLength;
|
|
361
|
+
progress();
|
|
362
|
+
} : undefined);
|
|
363
|
+
if (!options.store.appendPartial) {
|
|
364
|
+
transfer.downloadedBytes += downloaded.byteLength;
|
|
365
|
+
progress();
|
|
366
|
+
}
|
|
367
|
+
if (transfer.downloadedBytes > ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES)
|
|
368
|
+
throw new TypeError("Mobile update exceeds the maximum transfer size.");
|
|
369
|
+
const contents = combine(prefix, downloaded);
|
|
370
|
+
if (contents.byteLength !== file.bytes)
|
|
371
|
+
throw new TypeError(`Mobile update asset ${file.path} has an invalid size.`);
|
|
372
|
+
if (await options.verifier.digest(contents) !== file.sha256)
|
|
373
|
+
throw new TypeError(`Mobile update asset ${file.path} failed integrity verification.`);
|
|
374
|
+
await options.store.write(file, contents);
|
|
375
|
+
transfer.downloadedFiles += 1;
|
|
376
|
+
transfer.completedFiles += 1;
|
|
377
|
+
progress();
|
|
378
|
+
};
|
|
379
|
+
const worker = async () => {
|
|
380
|
+
if (firstError)
|
|
381
|
+
return;
|
|
382
|
+
const index = next++;
|
|
383
|
+
const file = manifest.files[index];
|
|
384
|
+
if (!file)
|
|
385
|
+
return;
|
|
386
|
+
try {
|
|
387
|
+
await downloadFile(file);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
firstError ??= error;
|
|
390
|
+
controller.abort();
|
|
391
|
+
}
|
|
392
|
+
await worker();
|
|
393
|
+
};
|
|
394
|
+
await Promise.all(Array.from({
|
|
395
|
+
length: Math.min(networkConcurrency(options.concurrency), manifest.files.length)
|
|
396
|
+
}, () => worker()));
|
|
397
|
+
if (firstError)
|
|
398
|
+
throw firstError;
|
|
399
|
+
updateTiming();
|
|
400
|
+
return transfer;
|
|
252
401
|
};
|
|
253
402
|
const check = async (download = false) => {
|
|
254
403
|
const response = await request(manifestUrl, {
|
|
@@ -270,39 +419,67 @@ var createAbsoluteMobileUpdateClient = (options) => {
|
|
|
270
419
|
throw new TypeError("Mobile update manifest is not valid JSON.");
|
|
271
420
|
}
|
|
272
421
|
const manifest = parseAbsoluteMobileUpdateManifest(manifestValue);
|
|
422
|
+
const healthToken = response.headers.get("x-absolute-mobile-health-token");
|
|
273
423
|
requireCompatible(manifest, options.config);
|
|
274
424
|
if (!await options.verifier.verify(manifest))
|
|
275
425
|
throw new TypeError("Mobile update signature verification failed.");
|
|
276
426
|
if (manifest.releaseId === options.config.currentReleaseId)
|
|
277
427
|
return { kind: "current" };
|
|
278
428
|
if (options.config.blockedReleaseIds?.includes(manifest.releaseId))
|
|
279
|
-
return {
|
|
429
|
+
return {
|
|
430
|
+
...healthToken ? { healthToken } : {},
|
|
431
|
+
kind: "quarantined",
|
|
432
|
+
releaseId: manifest.releaseId
|
|
433
|
+
};
|
|
280
434
|
if (!download)
|
|
281
|
-
return {
|
|
435
|
+
return {
|
|
436
|
+
...healthToken ? { healthToken } : {},
|
|
437
|
+
kind: "update-available",
|
|
438
|
+
manifest
|
|
439
|
+
};
|
|
282
440
|
await options.store.begin(manifest);
|
|
283
441
|
let transfer;
|
|
284
442
|
try {
|
|
285
443
|
transfer = await downloadFiles(manifest);
|
|
286
444
|
await options.store.commit(manifest);
|
|
287
445
|
} catch (error) {
|
|
288
|
-
|
|
446
|
+
if (options.store.suspend)
|
|
447
|
+
await options.store.suspend(manifest.releaseId);
|
|
448
|
+
else
|
|
449
|
+
await options.store.abort(manifest.releaseId);
|
|
450
|
+
if (healthToken)
|
|
451
|
+
reportAbsoluteMobileUpdateHealth(options.config, {
|
|
452
|
+
healthToken,
|
|
453
|
+
kind: "download-failed",
|
|
454
|
+
releaseId: manifest.releaseId
|
|
455
|
+
}, request).catch(() => {
|
|
456
|
+
return;
|
|
457
|
+
});
|
|
289
458
|
throw error;
|
|
290
459
|
}
|
|
291
|
-
return {
|
|
460
|
+
return {
|
|
461
|
+
...healthToken ? { healthToken } : {},
|
|
462
|
+
kind: "downloaded",
|
|
463
|
+
manifest,
|
|
464
|
+
transfer
|
|
465
|
+
};
|
|
292
466
|
};
|
|
293
467
|
return {
|
|
294
468
|
check,
|
|
295
469
|
activate: (releaseId) => options.store.activate(releaseId),
|
|
296
|
-
download: () => check(true)
|
|
470
|
+
download: () => check(true),
|
|
471
|
+
report: (input) => reportAbsoluteMobileUpdateHealth(options.config, input, request)
|
|
297
472
|
};
|
|
298
473
|
};
|
|
299
474
|
|
|
300
475
|
// src/mobile/shellUpdate.ts
|
|
301
476
|
var STATE_KEY = "absolute.mobile.update.state.v1";
|
|
477
|
+
var STAGING_KEY = "absolute.mobile.update.staging.v1";
|
|
302
478
|
var INSTALLATION_KEY = "absolute.mobile.update.installation.v1";
|
|
303
479
|
var RESULT_KEY = Symbol.for("absolutejs.mobile.update.result");
|
|
304
480
|
var RESULTS_KEY = Symbol.for("absolutejs.mobile.update.results");
|
|
305
481
|
var ROOT = "NoCloud/ionic_built_snapshots";
|
|
482
|
+
var STAGING_ROOT = "NoCloud/absolute_update_staging";
|
|
306
483
|
var watchdog = registerPlugin("AbsoluteMobileUpdateWatchdog");
|
|
307
484
|
var base64Bytes = (value) => Uint8Array.from(atob(value), (character) => character.charCodeAt(0));
|
|
308
485
|
var arrayBuffer = (value) => new Uint8Array(value).buffer;
|
|
@@ -348,12 +525,15 @@ var readState = async () => {
|
|
|
348
525
|
releaseId: recovery.releaseId
|
|
349
526
|
};
|
|
350
527
|
return {
|
|
528
|
+
...text("activeHealthToken") ? { activeHealthToken: text("activeHealthToken") } : {},
|
|
351
529
|
...text("activeRelease") ? { activeRelease: text("activeRelease") } : {},
|
|
352
530
|
...text("pendingRelease") ? { pendingRelease: text("pendingRelease") } : {},
|
|
531
|
+
...text("pendingHealthToken") ? { pendingHealthToken: text("pendingHealthToken") } : {},
|
|
353
532
|
...number("pendingStartedAt") === undefined ? {} : { pendingStartedAt: number("pendingStartedAt") },
|
|
354
533
|
...text("previousPath") ? { previousPath: text("previousPath") } : {},
|
|
355
534
|
...quarantinedReleases.length > 0 ? { quarantinedReleases } : {},
|
|
356
535
|
...text("readyRelease") ? { readyRelease: text("readyRelease") } : {},
|
|
536
|
+
...text("readyHealthToken") ? { readyHealthToken: text("readyHealthToken") } : {},
|
|
357
537
|
...validRecovery ? { recovery: validRecovery } : {}
|
|
358
538
|
};
|
|
359
539
|
} catch {
|
|
@@ -361,6 +541,25 @@ var readState = async () => {
|
|
|
361
541
|
}
|
|
362
542
|
};
|
|
363
543
|
var writeState = (state) => Preferences.set({ key: STATE_KEY, value: JSON.stringify(state) });
|
|
544
|
+
var clearStagingRoot = () => Filesystem.rmdir({
|
|
545
|
+
directory: Directory.Library,
|
|
546
|
+
path: STAGING_ROOT,
|
|
547
|
+
recursive: true
|
|
548
|
+
}).catch(() => {
|
|
549
|
+
return;
|
|
550
|
+
});
|
|
551
|
+
var readStaging = async () => {
|
|
552
|
+
const { value } = await Preferences.get({ key: STAGING_KEY });
|
|
553
|
+
if (!value)
|
|
554
|
+
return;
|
|
555
|
+
try {
|
|
556
|
+
return parseAbsoluteMobileUpdateManifest(JSON.parse(value));
|
|
557
|
+
} catch {
|
|
558
|
+
await Preferences.remove({ key: STAGING_KEY });
|
|
559
|
+
await clearStagingRoot();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
364
563
|
var installationId = async () => {
|
|
365
564
|
const existing = await Preferences.get({ key: INSTALLATION_KEY });
|
|
366
565
|
if (existing.value && /^[a-f0-9-]{36}$/u.test(existing.value))
|
|
@@ -379,6 +578,7 @@ var webView = () => {
|
|
|
379
578
|
};
|
|
380
579
|
var currentServerBasePath = () => new Promise((resolve) => webView().getServerBasePath(resolve));
|
|
381
580
|
var releasePath = (releaseId) => `${ROOT}/${releaseId}`;
|
|
581
|
+
var partialPath = (releaseId, file) => `${STAGING_ROOT}/${releaseId}/${file.path}.part`;
|
|
382
582
|
var releaseNativePath = async (releaseId) => {
|
|
383
583
|
const { uri } = await Filesystem.getUri({
|
|
384
584
|
directory: Directory.Library,
|
|
@@ -396,6 +596,15 @@ var removeRelease = async (releaseId) => {
|
|
|
396
596
|
return;
|
|
397
597
|
});
|
|
398
598
|
};
|
|
599
|
+
var removeStaging = async (releaseId) => {
|
|
600
|
+
await Filesystem.rmdir({
|
|
601
|
+
directory: Directory.Library,
|
|
602
|
+
path: `${STAGING_ROOT}/${releaseId}`,
|
|
603
|
+
recursive: true
|
|
604
|
+
}).catch(() => {
|
|
605
|
+
return;
|
|
606
|
+
});
|
|
607
|
+
};
|
|
399
608
|
var filesystemBytes = async (data) => typeof data === "string" ? base64Bytes(data) : new Uint8Array(await data.arrayBuffer());
|
|
400
609
|
var createStore = () => {
|
|
401
610
|
let staging;
|
|
@@ -403,9 +612,14 @@ var createStore = () => {
|
|
|
403
612
|
abort: async (releaseId) => {
|
|
404
613
|
staging = undefined;
|
|
405
614
|
await removeRelease(releaseId);
|
|
615
|
+
await removeStaging(releaseId);
|
|
616
|
+
const persisted = await readStaging();
|
|
617
|
+
if (persisted?.releaseId === releaseId)
|
|
618
|
+
await Preferences.remove({ key: STAGING_KEY });
|
|
406
619
|
const state = await readState();
|
|
407
620
|
if (state.readyRelease === releaseId || state.pendingRelease === releaseId)
|
|
408
621
|
await writeState({
|
|
622
|
+
activeHealthToken: state.activeHealthToken,
|
|
409
623
|
activeRelease: state.activeRelease,
|
|
410
624
|
quarantinedReleases: state.quarantinedReleases,
|
|
411
625
|
recovery: state.recovery
|
|
@@ -418,7 +632,9 @@ var createStore = () => {
|
|
|
418
632
|
const path = await releaseNativePath(releaseId);
|
|
419
633
|
const previousPath = await currentServerBasePath();
|
|
420
634
|
await writeState({
|
|
635
|
+
activeHealthToken: state.activeHealthToken,
|
|
421
636
|
activeRelease: state.activeRelease,
|
|
637
|
+
pendingHealthToken: state.readyHealthToken,
|
|
422
638
|
pendingRelease: releaseId,
|
|
423
639
|
pendingStartedAt: Date.now(),
|
|
424
640
|
previousPath,
|
|
@@ -433,19 +649,59 @@ var createStore = () => {
|
|
|
433
649
|
});
|
|
434
650
|
await removeRelease(releaseId);
|
|
435
651
|
await writeState({
|
|
652
|
+
activeHealthToken: state.activeHealthToken,
|
|
436
653
|
activeRelease: state.activeRelease,
|
|
437
654
|
quarantinedReleases: state.quarantinedReleases
|
|
438
655
|
});
|
|
439
656
|
throw error;
|
|
440
657
|
}
|
|
441
658
|
},
|
|
659
|
+
appendPartial: async (file, contents, offset) => {
|
|
660
|
+
if (!staging || !staging.files.some((candidate) => candidate.path === file.path))
|
|
661
|
+
throw new TypeError("Mobile update partial write is outside its staging transaction.");
|
|
662
|
+
const path = partialPath(staging.releaseId, file);
|
|
663
|
+
if (offset === 0) {
|
|
664
|
+
await Filesystem.writeFile({
|
|
665
|
+
data: bytesBase64(contents),
|
|
666
|
+
directory: Directory.Library,
|
|
667
|
+
path,
|
|
668
|
+
recursive: true
|
|
669
|
+
});
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const stat = await Filesystem.stat({
|
|
673
|
+
directory: Directory.Library,
|
|
674
|
+
path
|
|
675
|
+
});
|
|
676
|
+
if (stat.size !== offset)
|
|
677
|
+
throw new TypeError("Mobile update partial checkpoint changed unexpectedly.");
|
|
678
|
+
await Filesystem.appendFile({
|
|
679
|
+
data: bytesBase64(contents),
|
|
680
|
+
directory: Directory.Library,
|
|
681
|
+
path
|
|
682
|
+
});
|
|
683
|
+
},
|
|
442
684
|
begin: async (manifest) => {
|
|
443
|
-
await
|
|
685
|
+
const persisted = await readStaging();
|
|
686
|
+
const resume = persisted?.releaseId === manifest.releaseId && persisted.signature.keyId === manifest.signature.keyId && persisted.signature.value === manifest.signature.value;
|
|
687
|
+
if (persisted && !resume)
|
|
688
|
+
await Promise.all([
|
|
689
|
+
removeRelease(persisted.releaseId),
|
|
690
|
+
removeStaging(persisted.releaseId)
|
|
691
|
+
]);
|
|
692
|
+
if (!resume) {
|
|
693
|
+
await removeRelease(manifest.releaseId);
|
|
694
|
+
await removeStaging(manifest.releaseId);
|
|
695
|
+
}
|
|
444
696
|
await Filesystem.mkdir({
|
|
445
697
|
directory: Directory.Library,
|
|
446
698
|
path: releasePath(manifest.releaseId),
|
|
447
699
|
recursive: true
|
|
448
700
|
});
|
|
701
|
+
await Preferences.set({
|
|
702
|
+
key: STAGING_KEY,
|
|
703
|
+
value: JSON.stringify(manifest)
|
|
704
|
+
});
|
|
449
705
|
staging = manifest;
|
|
450
706
|
},
|
|
451
707
|
commit: async (manifest) => {
|
|
@@ -453,12 +709,24 @@ var createStore = () => {
|
|
|
453
709
|
throw new TypeError("Mobile update staging transaction changed identity.");
|
|
454
710
|
const state = await readState();
|
|
455
711
|
await writeState({
|
|
712
|
+
activeHealthToken: state.activeHealthToken,
|
|
456
713
|
activeRelease: state.activeRelease,
|
|
457
714
|
quarantinedReleases: state.quarantinedReleases,
|
|
458
715
|
readyRelease: manifest.releaseId
|
|
459
716
|
});
|
|
717
|
+
await removeStaging(manifest.releaseId);
|
|
718
|
+
await Preferences.remove({ key: STAGING_KEY });
|
|
460
719
|
staging = undefined;
|
|
461
720
|
},
|
|
721
|
+
readPartial: async (file) => {
|
|
722
|
+
if (!staging)
|
|
723
|
+
return null;
|
|
724
|
+
const result = await Filesystem.readFile({
|
|
725
|
+
directory: Directory.Library,
|
|
726
|
+
path: partialPath(staging.releaseId, file)
|
|
727
|
+
}).catch(() => null);
|
|
728
|
+
return result ? filesystemBytes(result.data).catch(() => null) : null;
|
|
729
|
+
},
|
|
462
730
|
readReusable: async (file) => {
|
|
463
731
|
const state = await readState();
|
|
464
732
|
if (!state.activeRelease)
|
|
@@ -469,6 +737,19 @@ var createStore = () => {
|
|
|
469
737
|
}).catch(() => null);
|
|
470
738
|
return result ? filesystemBytes(result.data).catch(() => null) : null;
|
|
471
739
|
},
|
|
740
|
+
readStaged: async (file) => {
|
|
741
|
+
if (!staging)
|
|
742
|
+
return null;
|
|
743
|
+
const result = await Filesystem.readFile({
|
|
744
|
+
directory: Directory.Library,
|
|
745
|
+
path: `${releasePath(staging.releaseId)}/${file.path}`
|
|
746
|
+
}).catch(() => null);
|
|
747
|
+
return result ? filesystemBytes(result.data).catch(() => null) : null;
|
|
748
|
+
},
|
|
749
|
+
suspend: async (releaseId) => {
|
|
750
|
+
if (staging?.releaseId === releaseId)
|
|
751
|
+
staging = undefined;
|
|
752
|
+
},
|
|
472
753
|
write: async (file, contents) => {
|
|
473
754
|
if (!staging || !staging.files.some((candidate) => candidate.path === file.path))
|
|
474
755
|
throw new TypeError("Mobile update write is outside its staging transaction.");
|
|
@@ -512,29 +793,56 @@ var removePriorRelease = async (prior, active) => {
|
|
|
512
793
|
if (prior && prior !== active)
|
|
513
794
|
await removeRelease(prior);
|
|
514
795
|
};
|
|
515
|
-
var reconcilePendingRelease = async (store, state) => {
|
|
796
|
+
var reconcilePendingRelease = async (store, state, report) => {
|
|
516
797
|
if (!state.pendingRelease)
|
|
517
798
|
return state;
|
|
518
799
|
const pendingPath = await releaseNativePath(state.pendingRelease);
|
|
519
800
|
const currentPath = await currentServerBasePath();
|
|
520
801
|
if (currentPath !== pendingPath) {
|
|
521
802
|
const failed = state.pendingRelease;
|
|
803
|
+
if (state.pendingHealthToken)
|
|
804
|
+
report({
|
|
805
|
+
healthToken: state.pendingHealthToken,
|
|
806
|
+
kind: "rolled-back",
|
|
807
|
+
releaseId: failed
|
|
808
|
+
});
|
|
522
809
|
await store.abort(failed);
|
|
523
810
|
emitUpdateResult({ kind: "rolled-back", releaseId: failed });
|
|
524
811
|
return readState();
|
|
525
812
|
}
|
|
526
813
|
webView().persistServerBasePath();
|
|
527
|
-
const next = {
|
|
814
|
+
const next = {
|
|
815
|
+
activeHealthToken: state.pendingHealthToken,
|
|
816
|
+
activeRelease: state.pendingRelease
|
|
817
|
+
};
|
|
528
818
|
await writeState(next);
|
|
529
819
|
await watchdog.confirm({ releaseId: state.pendingRelease });
|
|
820
|
+
if (state.pendingHealthToken)
|
|
821
|
+
report({
|
|
822
|
+
healthToken: state.pendingHealthToken,
|
|
823
|
+
kind: "activated",
|
|
824
|
+
releaseId: state.pendingRelease
|
|
825
|
+
});
|
|
530
826
|
await removePriorRelease(state.activeRelease, next.activeRelease);
|
|
531
827
|
emitUpdateResult({ kind: "activated", releaseId: next.activeRelease });
|
|
532
828
|
return next;
|
|
533
829
|
};
|
|
534
|
-
var consumeNativeRecovery = async (state) => {
|
|
830
|
+
var consumeNativeRecovery = async (state, report) => {
|
|
535
831
|
if (!state.recovery)
|
|
536
832
|
return state;
|
|
537
|
-
const {
|
|
833
|
+
const {
|
|
834
|
+
pendingHealthToken,
|
|
835
|
+
readyHealthToken: _readyHealthToken,
|
|
836
|
+
recovery,
|
|
837
|
+
...next
|
|
838
|
+
} = state;
|
|
839
|
+
if (pendingHealthToken)
|
|
840
|
+
report({
|
|
841
|
+
healthToken: pendingHealthToken,
|
|
842
|
+
kind: "rolled-back",
|
|
843
|
+
reason: recovery.reason,
|
|
844
|
+
releaseId: recovery.releaseId
|
|
845
|
+
});
|
|
538
846
|
emitUpdateResult({
|
|
539
847
|
durationMs: Math.round(recovery.durationMs),
|
|
540
848
|
kind: "rolled-back",
|
|
@@ -545,34 +853,66 @@ var consumeNativeRecovery = async (state) => {
|
|
|
545
853
|
return next;
|
|
546
854
|
};
|
|
547
855
|
var installAbsoluteMobileShellUpdates = async (manifest) => {
|
|
548
|
-
|
|
856
|
+
const { updates } = manifest;
|
|
857
|
+
if (!updates)
|
|
549
858
|
return;
|
|
550
859
|
const store = createStore();
|
|
551
|
-
const
|
|
552
|
-
const
|
|
860
|
+
const identity = await installationId();
|
|
861
|
+
const clientConfig = (state2) => ({
|
|
862
|
+
appId: manifest.appId,
|
|
863
|
+
blockedReleaseIds: state2.quarantinedReleases ?? [],
|
|
864
|
+
channel: updates.channel,
|
|
865
|
+
currentReleaseId: state2.activeRelease ?? `embedded:${manifest.appBuild}`,
|
|
866
|
+
installationId: identity,
|
|
867
|
+
manifestUrl: updates.manifestUrl,
|
|
868
|
+
runtimeFingerprint: manifest.nativeRuntime
|
|
869
|
+
});
|
|
870
|
+
const reportFor = (state2) => async (evidence) => {
|
|
871
|
+
await reportAbsoluteMobileUpdateHealth(clientConfig(state2), evidence).catch(() => {
|
|
872
|
+
return;
|
|
873
|
+
});
|
|
874
|
+
};
|
|
875
|
+
const initial = await readState();
|
|
876
|
+
const recovered = await consumeNativeRecovery(initial, reportFor(initial));
|
|
877
|
+
const state = await reconcilePendingRelease(store, recovered, reportFor(recovered));
|
|
553
878
|
const client = createAbsoluteMobileUpdateClient({
|
|
554
|
-
config:
|
|
555
|
-
appId: manifest.appId,
|
|
556
|
-
blockedReleaseIds: state.quarantinedReleases ?? [],
|
|
557
|
-
channel: manifest.updates.channel,
|
|
558
|
-
currentReleaseId: state.activeRelease ?? `embedded:${manifest.appBuild}`,
|
|
559
|
-
installationId: await installationId(),
|
|
560
|
-
manifestUrl: manifest.updates.manifestUrl,
|
|
561
|
-
runtimeFingerprint: manifest.nativeRuntime
|
|
562
|
-
},
|
|
879
|
+
config: clientConfig(state),
|
|
563
880
|
store,
|
|
564
|
-
verifier: createVerifier(
|
|
881
|
+
verifier: createVerifier(updates.publicKeys),
|
|
882
|
+
onProgress: (progress) => emitUpdateResult(progress)
|
|
565
883
|
});
|
|
566
884
|
const activateDownloaded = async (result) => {
|
|
567
885
|
if (result.kind !== "downloaded")
|
|
568
886
|
return;
|
|
887
|
+
if (result.healthToken) {
|
|
888
|
+
const ready = await readState();
|
|
889
|
+
if (ready.readyRelease === result.manifest.releaseId)
|
|
890
|
+
await writeState({
|
|
891
|
+
...ready,
|
|
892
|
+
readyHealthToken: result.healthToken
|
|
893
|
+
});
|
|
894
|
+
client.report({
|
|
895
|
+
healthToken: result.healthToken,
|
|
896
|
+
kind: "downloaded",
|
|
897
|
+
releaseId: result.manifest.releaseId,
|
|
898
|
+
transfer: result.transfer
|
|
899
|
+
}).catch(() => {
|
|
900
|
+
return;
|
|
901
|
+
});
|
|
902
|
+
}
|
|
569
903
|
emitUpdateResult({
|
|
904
|
+
avoidedBytes: result.transfer.avoidedBytes,
|
|
905
|
+
completedFiles: result.transfer.completedFiles,
|
|
570
906
|
downloadedBytes: result.transfer.downloadedBytes,
|
|
571
907
|
downloadedFiles: result.transfer.downloadedFiles,
|
|
908
|
+
durationMs: Math.round(result.transfer.durationMs),
|
|
572
909
|
kind: "downloaded",
|
|
573
910
|
releaseId: result.manifest.releaseId,
|
|
911
|
+
resumedBytes: result.transfer.resumedBytes,
|
|
912
|
+
resumedFiles: result.transfer.resumedFiles,
|
|
574
913
|
reusedBytes: result.transfer.reusedBytes,
|
|
575
914
|
reusedFiles: result.transfer.reusedFiles,
|
|
915
|
+
throughputBytesPerSecond: result.transfer.throughputBytesPerSecond,
|
|
576
916
|
totalBytes: result.transfer.totalBytes,
|
|
577
917
|
totalFiles: result.transfer.totalFiles
|
|
578
918
|
});
|
|
@@ -580,6 +920,14 @@ var installAbsoluteMobileShellUpdates = async (manifest) => {
|
|
|
580
920
|
};
|
|
581
921
|
client.download().then((result) => {
|
|
582
922
|
if (result.kind === "quarantined") {
|
|
923
|
+
if (result.healthToken)
|
|
924
|
+
client.report({
|
|
925
|
+
healthToken: result.healthToken,
|
|
926
|
+
kind: "quarantined",
|
|
927
|
+
releaseId: result.releaseId
|
|
928
|
+
}).catch(() => {
|
|
929
|
+
return;
|
|
930
|
+
});
|
|
583
931
|
emitUpdateResult({
|
|
584
932
|
kind: "quarantined",
|
|
585
933
|
releaseId: result.releaseId
|