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

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);
@@ -261,6 +264,7 @@ var createAbsoluteMobileUpdateClient = (options) => {
261
264
  var STATE_KEY = "absolute.mobile.update.state.v1";
262
265
  var INSTALLATION_KEY = "absolute.mobile.update.installation.v1";
263
266
  var ROOT = "NoCloud/ionic_built_snapshots";
267
+ var watchdog = registerPlugin("AbsoluteMobileUpdateWatchdog");
264
268
  var base64Bytes = (value) => Uint8Array.from(atob(value), (character) => character.charCodeAt(0));
265
269
  var arrayBuffer = (value) => new Uint8Array(value).buffer;
266
270
  var bytesBase64 = (value) => {
@@ -283,10 +287,35 @@ var readState = async () => {
283
287
  const candidate = Reflect.get(parsed, key);
284
288
  return typeof candidate === "string" ? candidate : undefined;
285
289
  };
290
+ const number = (key) => {
291
+ const candidate = Reflect.get(parsed, key);
292
+ return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : undefined;
293
+ };
294
+ const recoveryValue = Reflect.get(parsed, "recovery");
295
+ const quarantineValue = Reflect.get(parsed, "quarantinedReleases");
296
+ const quarantinedReleases = Array.isArray(quarantineValue) ? [
297
+ ...new Set(quarantineValue.filter((candidateRelease) => typeof candidateRelease === "string" && /^amu_[a-f0-9]{64}$/u.test(candidateRelease)))
298
+ ].slice(-8) : [];
299
+ const recovery = typeof recoveryValue === "object" && recoveryValue !== null ? {
300
+ durationMs: Reflect.get(recoveryValue, "durationMs"),
301
+ reason: Reflect.get(recoveryValue, "reason"),
302
+ releaseId: Reflect.get(recoveryValue, "releaseId")
303
+ } : undefined;
304
+ let validRecovery;
305
+ 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")
306
+ validRecovery = {
307
+ durationMs: recovery.durationMs,
308
+ reason: recovery.reason,
309
+ releaseId: recovery.releaseId
310
+ };
286
311
  return {
287
312
  ...text("activeRelease") ? { activeRelease: text("activeRelease") } : {},
288
313
  ...text("pendingRelease") ? { pendingRelease: text("pendingRelease") } : {},
289
- ...text("readyRelease") ? { readyRelease: text("readyRelease") } : {}
314
+ ...number("pendingStartedAt") === undefined ? {} : { pendingStartedAt: number("pendingStartedAt") },
315
+ ...text("previousPath") ? { previousPath: text("previousPath") } : {},
316
+ ...quarantinedReleases.length > 0 ? { quarantinedReleases } : {},
317
+ ...text("readyRelease") ? { readyRelease: text("readyRelease") } : {},
318
+ ...validRecovery ? { recovery: validRecovery } : {}
290
319
  };
291
320
  } catch {
292
321
  return {};
@@ -336,18 +365,39 @@ var createStore = () => {
336
365
  await removeRelease(releaseId);
337
366
  const state = await readState();
338
367
  if (state.readyRelease === releaseId || state.pendingRelease === releaseId)
339
- await writeState({ activeRelease: state.activeRelease });
368
+ await writeState({
369
+ activeRelease: state.activeRelease,
370
+ quarantinedReleases: state.quarantinedReleases,
371
+ recovery: state.recovery
372
+ });
340
373
  },
341
374
  activate: async (releaseId) => {
342
375
  const state = await readState();
343
376
  if (state.readyRelease !== releaseId)
344
377
  throw new TypeError("Mobile update is not committed and ready to activate.");
345
378
  const path = await releaseNativePath(releaseId);
379
+ const previousPath = await currentServerBasePath();
346
380
  await writeState({
347
381
  activeRelease: state.activeRelease,
348
- pendingRelease: releaseId
382
+ pendingRelease: releaseId,
383
+ pendingStartedAt: Date.now(),
384
+ previousPath,
385
+ quarantinedReleases: state.quarantinedReleases
349
386
  });
350
- webView().setServerBasePath(path);
387
+ try {
388
+ await watchdog.arm({ releaseId });
389
+ webView().setServerBasePath(path);
390
+ } catch (error) {
391
+ await watchdog.confirm({ releaseId }).catch(() => {
392
+ return;
393
+ });
394
+ await removeRelease(releaseId);
395
+ await writeState({
396
+ activeRelease: state.activeRelease,
397
+ quarantinedReleases: state.quarantinedReleases
398
+ });
399
+ throw error;
400
+ }
351
401
  },
352
402
  begin: async (manifest) => {
353
403
  await removeRelease(manifest.releaseId);
@@ -364,6 +414,7 @@ var createStore = () => {
364
414
  const state = await readState();
365
415
  await writeState({
366
416
  activeRelease: state.activeRelease,
417
+ quarantinedReleases: state.quarantinedReleases,
367
418
  readyRelease: manifest.releaseId
368
419
  });
369
420
  staging = undefined;
@@ -416,18 +467,34 @@ var reconcilePendingRelease = async (store, state) => {
416
467
  webView().persistServerBasePath();
417
468
  const next = { activeRelease: state.pendingRelease };
418
469
  await writeState(next);
470
+ await watchdog.confirm({ releaseId: state.pendingRelease });
419
471
  await removePriorRelease(state.activeRelease, next.activeRelease);
420
472
  emitUpdateResult({ kind: "activated", releaseId: next.activeRelease });
421
473
  return next;
422
474
  };
475
+ var consumeNativeRecovery = async (state) => {
476
+ if (!state.recovery)
477
+ return state;
478
+ const { recovery, ...next } = state;
479
+ emitUpdateResult({
480
+ durationMs: Math.round(recovery.durationMs),
481
+ kind: "rolled-back",
482
+ reason: recovery.reason,
483
+ releaseId: recovery.releaseId
484
+ });
485
+ await writeState(next);
486
+ return next;
487
+ };
423
488
  var installAbsoluteMobileShellUpdates = async (manifest) => {
424
489
  if (!manifest.updates)
425
490
  return;
426
491
  const store = createStore();
427
- const state = await reconcilePendingRelease(store, await readState());
492
+ const recovered = await consumeNativeRecovery(await readState());
493
+ const state = await reconcilePendingRelease(store, recovered);
428
494
  const client = createAbsoluteMobileUpdateClient({
429
495
  config: {
430
496
  appId: manifest.appId,
497
+ blockedReleaseIds: state.quarantinedReleases ?? [],
431
498
  channel: manifest.updates.channel,
432
499
  currentReleaseId: state.activeRelease ?? `embedded:${manifest.appBuild}`,
433
500
  installationId: await installationId(),
@@ -446,7 +513,16 @@ var installAbsoluteMobileShellUpdates = async (manifest) => {
446
513
  });
447
514
  await client.activate(result.manifest.releaseId);
448
515
  };
449
- client.download().then(activateDownloaded).catch((error) => {
516
+ client.download().then((result) => {
517
+ if (result.kind === "quarantined") {
518
+ emitUpdateResult({
519
+ kind: "quarantined",
520
+ releaseId: result.releaseId
521
+ });
522
+ return;
523
+ }
524
+ return activateDownloaded(result);
525
+ }).catch((error) => {
450
526
  console.error("[Absolute Mobile] Update failed:", error);
451
527
  emitUpdateResult({ kind: "failed" });
452
528
  });
@@ -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.61",
297
+ "@absolutejs/native-darwin-x64": "0.20.0-beta.61",
298
+ "@absolutejs/native-linux-arm64": "0.20.0-beta.61",
299
+ "@absolutejs/native-linux-x64": "0.20.0-beta.61",
300
+ "@absolutejs/native-windows-arm64": "0.20.0-beta.61",
301
+ "@absolutejs/native-windows-x64": "0.20.0-beta.61"
302
302
  },
303
303
  "overrides": {
304
304
  "@sinclair/typebox": "0.34.52",
@@ -514,7 +514,7 @@
514
514
  ]
515
515
  }
516
516
  },
517
- "version": "0.20.0-beta.60",
517
+ "version": "0.20.0-beta.61",
518
518
  "workspaces": [
519
519
  "tests/fixtures/*",
520
520
  "tests/fixtures/_packages/*"