@absolutejs/absolute 0.20.0-beta.60 → 0.20.0-beta.62

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.
@@ -1,6 +1,7 @@
1
1
  // src/mobile/shellUpdate.ts
2
2
  import { Directory, Filesystem } from "@capacitor/filesystem";
3
3
  import { Preferences } from "@capacitor/preferences";
4
+ import { registerPlugin } from "@capacitor/core";
4
5
 
5
6
  // src/mobile/updateProtocol.ts
6
7
  var ABSOLUTE_MOBILE_UPDATE_FORMAT = 1;
@@ -238,6 +239,8 @@ var createAbsoluteMobileUpdateClient = (options) => {
238
239
  throw new TypeError("Mobile update signature verification failed.");
239
240
  if (manifest.releaseId === options.config.currentReleaseId)
240
241
  return { kind: "current" };
242
+ if (options.config.blockedReleaseIds?.includes(manifest.releaseId))
243
+ return { kind: "quarantined", releaseId: manifest.releaseId };
241
244
  if (!download)
242
245
  return { kind: "update-available", manifest };
243
246
  await options.store.begin(manifest);
@@ -260,7 +263,10 @@ var createAbsoluteMobileUpdateClient = (options) => {
260
263
  // src/mobile/shellUpdate.ts
261
264
  var STATE_KEY = "absolute.mobile.update.state.v1";
262
265
  var INSTALLATION_KEY = "absolute.mobile.update.installation.v1";
266
+ var RESULT_KEY = Symbol.for("absolutejs.mobile.update.result");
267
+ var RESULTS_KEY = Symbol.for("absolutejs.mobile.update.results");
263
268
  var ROOT = "NoCloud/ionic_built_snapshots";
269
+ var watchdog = registerPlugin("AbsoluteMobileUpdateWatchdog");
264
270
  var base64Bytes = (value) => Uint8Array.from(atob(value), (character) => character.charCodeAt(0));
265
271
  var arrayBuffer = (value) => new Uint8Array(value).buffer;
266
272
  var bytesBase64 = (value) => {
@@ -283,10 +289,35 @@ var readState = async () => {
283
289
  const candidate = Reflect.get(parsed, key);
284
290
  return typeof candidate === "string" ? candidate : undefined;
285
291
  };
292
+ const number = (key) => {
293
+ const candidate = Reflect.get(parsed, key);
294
+ return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : undefined;
295
+ };
296
+ const recoveryValue = Reflect.get(parsed, "recovery");
297
+ const quarantineValue = Reflect.get(parsed, "quarantinedReleases");
298
+ const quarantinedReleases = Array.isArray(quarantineValue) ? [
299
+ ...new Set(quarantineValue.filter((candidateRelease) => typeof candidateRelease === "string" && /^amu_[a-f0-9]{64}$/u.test(candidateRelease)))
300
+ ].slice(-8) : [];
301
+ const recovery = typeof recoveryValue === "object" && recoveryValue !== null ? {
302
+ durationMs: Reflect.get(recoveryValue, "durationMs"),
303
+ reason: Reflect.get(recoveryValue, "reason"),
304
+ releaseId: Reflect.get(recoveryValue, "releaseId")
305
+ } : undefined;
306
+ let validRecovery;
307
+ if (recovery && typeof recovery.durationMs === "number" && Number.isFinite(recovery.durationMs) && recovery.durationMs >= 0 && (recovery.reason === "boot-interrupted" || recovery.reason === "boot-timeout") && typeof recovery.releaseId === "string")
308
+ validRecovery = {
309
+ durationMs: recovery.durationMs,
310
+ reason: recovery.reason,
311
+ releaseId: recovery.releaseId
312
+ };
286
313
  return {
287
314
  ...text("activeRelease") ? { activeRelease: text("activeRelease") } : {},
288
315
  ...text("pendingRelease") ? { pendingRelease: text("pendingRelease") } : {},
289
- ...text("readyRelease") ? { readyRelease: text("readyRelease") } : {}
316
+ ...number("pendingStartedAt") === undefined ? {} : { pendingStartedAt: number("pendingStartedAt") },
317
+ ...text("previousPath") ? { previousPath: text("previousPath") } : {},
318
+ ...quarantinedReleases.length > 0 ? { quarantinedReleases } : {},
319
+ ...text("readyRelease") ? { readyRelease: text("readyRelease") } : {},
320
+ ...validRecovery ? { recovery: validRecovery } : {}
290
321
  };
291
322
  } catch {
292
323
  return {};
@@ -336,18 +367,39 @@ var createStore = () => {
336
367
  await removeRelease(releaseId);
337
368
  const state = await readState();
338
369
  if (state.readyRelease === releaseId || state.pendingRelease === releaseId)
339
- await writeState({ activeRelease: state.activeRelease });
370
+ await writeState({
371
+ activeRelease: state.activeRelease,
372
+ quarantinedReleases: state.quarantinedReleases,
373
+ recovery: state.recovery
374
+ });
340
375
  },
341
376
  activate: async (releaseId) => {
342
377
  const state = await readState();
343
378
  if (state.readyRelease !== releaseId)
344
379
  throw new TypeError("Mobile update is not committed and ready to activate.");
345
380
  const path = await releaseNativePath(releaseId);
381
+ const previousPath = await currentServerBasePath();
346
382
  await writeState({
347
383
  activeRelease: state.activeRelease,
348
- pendingRelease: releaseId
384
+ pendingRelease: releaseId,
385
+ pendingStartedAt: Date.now(),
386
+ previousPath,
387
+ quarantinedReleases: state.quarantinedReleases
349
388
  });
350
- webView().setServerBasePath(path);
389
+ try {
390
+ await watchdog.arm({ releaseId });
391
+ webView().setServerBasePath(path);
392
+ } catch (error) {
393
+ await watchdog.confirm({ releaseId }).catch(() => {
394
+ return;
395
+ });
396
+ await removeRelease(releaseId);
397
+ await writeState({
398
+ activeRelease: state.activeRelease,
399
+ quarantinedReleases: state.quarantinedReleases
400
+ });
401
+ throw error;
402
+ }
351
403
  },
352
404
  begin: async (manifest) => {
353
405
  await removeRelease(manifest.releaseId);
@@ -364,6 +416,7 @@ var createStore = () => {
364
416
  const state = await readState();
365
417
  await writeState({
366
418
  activeRelease: state.activeRelease,
419
+ quarantinedReleases: state.quarantinedReleases,
367
420
  readyRelease: manifest.releaseId
368
421
  });
369
422
  staging = undefined;
@@ -397,7 +450,16 @@ var createVerifier = (publicKeys) => ({
397
450
  }
398
451
  }
399
452
  });
400
- var emitUpdateResult = (detail) => dispatchEvent(new CustomEvent("absolute:mobile-update", { detail }));
453
+ var emitUpdateResult = (detail) => {
454
+ Reflect.set(globalThis, RESULT_KEY, detail);
455
+ const existing = Reflect.get(globalThis, RESULTS_KEY);
456
+ const results = Array.isArray(existing) ? existing : [];
457
+ results.push(detail);
458
+ if (results.length > 8)
459
+ results.splice(0, results.length - 8);
460
+ Reflect.set(globalThis, RESULTS_KEY, results);
461
+ dispatchEvent(new CustomEvent("absolute:mobile-update", { detail }));
462
+ };
401
463
  var removePriorRelease = async (prior, active) => {
402
464
  if (prior && prior !== active)
403
465
  await removeRelease(prior);
@@ -416,18 +478,34 @@ var reconcilePendingRelease = async (store, state) => {
416
478
  webView().persistServerBasePath();
417
479
  const next = { activeRelease: state.pendingRelease };
418
480
  await writeState(next);
481
+ await watchdog.confirm({ releaseId: state.pendingRelease });
419
482
  await removePriorRelease(state.activeRelease, next.activeRelease);
420
483
  emitUpdateResult({ kind: "activated", releaseId: next.activeRelease });
421
484
  return next;
422
485
  };
486
+ var consumeNativeRecovery = async (state) => {
487
+ if (!state.recovery)
488
+ return state;
489
+ const { recovery, ...next } = state;
490
+ emitUpdateResult({
491
+ durationMs: Math.round(recovery.durationMs),
492
+ kind: "rolled-back",
493
+ reason: recovery.reason,
494
+ releaseId: recovery.releaseId
495
+ });
496
+ await writeState(next);
497
+ return next;
498
+ };
423
499
  var installAbsoluteMobileShellUpdates = async (manifest) => {
424
500
  if (!manifest.updates)
425
501
  return;
426
502
  const store = createStore();
427
- const state = await reconcilePendingRelease(store, await readState());
503
+ const recovered = await consumeNativeRecovery(await readState());
504
+ const state = await reconcilePendingRelease(store, recovered);
428
505
  const client = createAbsoluteMobileUpdateClient({
429
506
  config: {
430
507
  appId: manifest.appId,
508
+ blockedReleaseIds: state.quarantinedReleases ?? [],
431
509
  channel: manifest.updates.channel,
432
510
  currentReleaseId: state.activeRelease ?? `embedded:${manifest.appBuild}`,
433
511
  installationId: await installationId(),
@@ -446,7 +524,16 @@ var installAbsoluteMobileShellUpdates = async (manifest) => {
446
524
  });
447
525
  await client.activate(result.manifest.releaseId);
448
526
  };
449
- client.download().then(activateDownloaded).catch((error) => {
527
+ client.download().then((result) => {
528
+ if (result.kind === "quarantined") {
529
+ emitUpdateResult({
530
+ kind: "quarantined",
531
+ releaseId: result.releaseId
532
+ });
533
+ return;
534
+ }
535
+ return activateDownloaded(result);
536
+ }).catch((error) => {
450
537
  console.error("[Absolute Mobile] Update failed:", error);
451
538
  emitUpdateResult({ kind: "failed" });
452
539
  });
@@ -24,6 +24,7 @@ export declare const materializeAbsoluteCapacitorWebBundle: (options: AbsoluteCa
24
24
  storageSchema: SyncLocalStoreSchemaBundle;
25
25
  } | undefined;
26
26
  updates?: {
27
+ bootTimeoutMs: number;
27
28
  channel: string;
28
29
  manifestUrl: string;
29
30
  publicKeys: Record<string, string>;
@@ -17,6 +17,7 @@ export type NormalizedAbsoluteMobileConfig = {
17
17
  productionOrigin: string;
18
18
  pushAndroidGoogleServicesFile: string;
19
19
  updates?: {
20
+ bootTimeoutMs: number;
20
21
  channel: string;
21
22
  manifestUrl: string;
22
23
  publicKeys: Record<string, string>;
@@ -24,6 +24,7 @@ export type AbsoluteMobileClientManifest = {
24
24
  routes: AbsoluteMobileCompatibilityRoute[];
25
25
  runtime: string;
26
26
  updates?: {
27
+ bootTimeoutMs: number;
27
28
  channel: string;
28
29
  manifestUrl: string;
29
30
  publicKeys: Record<string, string>;
@@ -1,6 +1,7 @@
1
1
  import { type AbsoluteMobileUpdateFile, type AbsoluteMobileUpdateManifest } from './updateProtocol';
2
2
  export type AbsoluteMobileUpdateClientConfig = {
3
3
  appId: string;
4
+ blockedReleaseIds?: readonly string[];
4
5
  channel: string;
5
6
  currentReleaseId: string;
6
7
  installationId: string;
@@ -29,6 +30,9 @@ export type AbsoluteMobileUpdateCheckResult = {
29
30
  } | {
30
31
  kind: 'downloaded';
31
32
  manifest: AbsoluteMobileUpdateManifest;
33
+ } | {
34
+ kind: 'quarantined';
35
+ releaseId: string;
32
36
  } | {
33
37
  kind: 'update-available';
34
38
  manifest: AbsoluteMobileUpdateManifest;
@@ -2,7 +2,7 @@ import type { AbsoluteDeviceCapabilityPlan } from './deviceCapabilities';
2
2
  import type { NormalizedAbsoluteMobileConfig } from './config';
3
3
  import type { AbsoluteMobileAuthManifest } from './nativeAuth';
4
4
  import type { SyncLocalStoreSchemaBundle } from '@absolutejs/sync/client';
5
- export declare const ABSOLUTE_MOBILE_SHELL_ABI: 1;
5
+ export declare const ABSOLUTE_MOBILE_SHELL_ABI: 2;
6
6
  export declare const ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT: 1;
7
7
  export type AbsoluteMobileUpdateRuntimeDescriptor = {
8
8
  appId: string;
@@ -57,6 +57,8 @@ type MobileSharedConfig = {
57
57
  };
58
58
  /** Signed over-the-air web-bundle updates. Native capability changes still require a store build. */
59
59
  updates?: {
60
+ /** Maximum time an activated bundle may take to render its first page before native rollback. Defaults to 20 seconds. */
61
+ bootTimeoutMs?: number;
60
62
  /** Release channel. Defaults to `production`. */
61
63
  channel?: string;
62
64
  /** Exact signed-manifest URL. Defaults to the AbsoluteJS production origin. */
package/package.json CHANGED
@@ -293,12 +293,12 @@
293
293
  "main": "./dist/index.js",
294
294
  "name": "@absolutejs/absolute",
295
295
  "optionalDependencies": {
296
- "@absolutejs/native-darwin-arm64": "0.20.0-beta.60",
297
- "@absolutejs/native-darwin-x64": "0.20.0-beta.60",
298
- "@absolutejs/native-linux-arm64": "0.20.0-beta.60",
299
- "@absolutejs/native-linux-x64": "0.20.0-beta.60",
300
- "@absolutejs/native-windows-arm64": "0.20.0-beta.60",
301
- "@absolutejs/native-windows-x64": "0.20.0-beta.60"
296
+ "@absolutejs/native-darwin-arm64": "0.20.0-beta.62",
297
+ "@absolutejs/native-darwin-x64": "0.20.0-beta.62",
298
+ "@absolutejs/native-linux-arm64": "0.20.0-beta.62",
299
+ "@absolutejs/native-linux-x64": "0.20.0-beta.62",
300
+ "@absolutejs/native-windows-arm64": "0.20.0-beta.62",
301
+ "@absolutejs/native-windows-x64": "0.20.0-beta.62"
302
302
  },
303
303
  "overrides": {
304
304
  "@sinclair/typebox": "0.34.52",
@@ -460,6 +460,7 @@
460
460
  "test:native:android:bundle": "ABSOLUTE_TEST_NATIVE_ANDROID=1 bun test tests/native/android-embedded-bundle-conformance.test.ts",
461
461
  "test:native:android:hmr": "ABSOLUTE_TEST_NATIVE_ANDROID=1 bun test tests/native/android-hmr-conformance.test.ts",
462
462
  "test:native:android:lifecycle": "ABSOLUTE_TEST_NATIVE_ANDROID=1 bun test tests/native/android-lifecycle-conformance.test.ts",
463
+ "test:native:android:updates": "ABSOLUTE_TEST_NATIVE_ANDROID=1 bun test tests/native/android-embedded-bundle-conformance.test.ts -t 'signed web updates'",
463
464
  "test:native:android:upgrade": "ABSOLUTE_TEST_NATIVE_ANDROID=1 bun test tests/native/android-embedded-bundle-conformance.test.ts -t 'installed APK upgrade'",
464
465
  "test:native:ios": "bun run test:native:ios:lifecycle && bun run test:native:ios:bundle",
465
466
  "test:native:ios:bundle": "ABSOLUTE_TEST_NATIVE_IOS=1 bun test tests/native/ios-embedded-bundle-conformance.test.ts",
@@ -514,7 +515,7 @@
514
515
  ]
515
516
  }
516
517
  },
517
- "version": "0.20.0-beta.60",
518
+ "version": "0.20.0-beta.62",
518
519
  "workspaces": [
519
520
  "tests/fixtures/*",
520
521
  "tests/fixtures/_packages/*"