feat: execute route and release recovery contracts
This commit is contained in:
@@ -5,6 +5,7 @@ import type {
|
||||
RenderFailureReport,
|
||||
} from "./ports/in/application-api.js";
|
||||
import type { ApplicationOutputPorts } from "./ports/out/application-output-ports.js";
|
||||
import { decideChunkRecovery } from "./use-cases/decide-chunk-recovery.js";
|
||||
|
||||
export type { ApplicationApi, ApplicationOutputPorts };
|
||||
|
||||
@@ -64,10 +65,54 @@ export function createApplication(
|
||||
},
|
||||
});
|
||||
|
||||
const recovery = Object.freeze({
|
||||
async recoverChunk(input: {
|
||||
chunkId: string;
|
||||
failureKind: "CHUNK_LOAD_FAILURE" | "DEPLOY_MISMATCH";
|
||||
}) {
|
||||
try {
|
||||
const current = await outputPorts.releaseInfo.getCurrent();
|
||||
const active = await outputPorts.releaseInfo.refresh();
|
||||
if (!active.routeChunks[input.chunkId]) {
|
||||
return {
|
||||
action: "support" as const,
|
||||
reason: "active-chunk-unknown",
|
||||
};
|
||||
}
|
||||
const decision = decideChunkRecovery({
|
||||
failureKind: input.failureKind,
|
||||
manifestLoaded: true,
|
||||
currentBuildId: current.buildId,
|
||||
currentReleaseId: current.releaseId,
|
||||
activeBuildId: active.buildId,
|
||||
activeReleaseId: active.releaseId,
|
||||
storage: outputPorts.preferences,
|
||||
});
|
||||
if (decision.action === "reload-once") {
|
||||
try {
|
||||
outputPorts.navigation.reload();
|
||||
} catch {
|
||||
return {
|
||||
action: "support" as const,
|
||||
reason: "reload-failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
return decision;
|
||||
} catch {
|
||||
return {
|
||||
action: "support" as const,
|
||||
reason: "manifest-unavailable",
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return Object.freeze({
|
||||
session,
|
||||
preferences,
|
||||
diagnostics,
|
||||
runtime,
|
||||
recovery,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,4 +38,13 @@ export type ApplicationApi = Readonly<{
|
||||
runtime: Readonly<{
|
||||
getReleaseSummary(): Promise<ReleaseSummary>;
|
||||
}>;
|
||||
recovery: Readonly<{
|
||||
recoverChunk(input: Readonly<{
|
||||
chunkId: string;
|
||||
failureKind: "CHUNK_LOAD_FAILURE" | "DEPLOY_MISMATCH";
|
||||
}>): Promise<
|
||||
| Readonly<{ action: "reload-once"; releasePair: string }>
|
||||
| Readonly<{ action: "support"; reason: string }>
|
||||
>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
@@ -15,4 +15,5 @@ export type ApplicationOutputPorts = Readonly<{
|
||||
preferences: StoragePort;
|
||||
diagnostics: TelemetryPort;
|
||||
releaseInfo: ReleaseInfoPort;
|
||||
navigation: Readonly<{ reload(): void }>;
|
||||
}>;
|
||||
|
||||
@@ -9,7 +9,16 @@
|
||||
* apiContractVersion: string,
|
||||
* assetManifestHash: string,
|
||||
* releaseId: string,
|
||||
* builtAt?: string
|
||||
* builtAt?: string,
|
||||
* routeChunks: Record<string, string>
|
||||
* }>,
|
||||
* refresh(): Promise<{
|
||||
* buildId: string,
|
||||
* configSchemaVersion: string,
|
||||
* apiContractVersion: string,
|
||||
* assetManifestHash: string,
|
||||
* releaseId: string,
|
||||
* routeChunks: Record<string, string>
|
||||
* }>
|
||||
* }} ReleaseInfoPort
|
||||
*/
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
const RECOVERABLE_KINDS = new Set(["CHUNK_LOAD_FAILURE", "DEPLOY_MISMATCH"]);
|
||||
|
||||
/**
|
||||
* @typedef {{action: "reload-once", releasePair: string} |
|
||||
* {action: "support", reason: string}} ChunkRecoveryDecision
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* failureKind: string,
|
||||
* manifestLoaded: boolean,
|
||||
* currentBuildId: string,
|
||||
* currentReleaseId: string,
|
||||
* activeBuildId: string,
|
||||
* activeReleaseId: string,
|
||||
* storage: import("../ports/storage-port.js").StoragePort
|
||||
* }} input
|
||||
* @returns {ChunkRecoveryDecision}
|
||||
*/
|
||||
export function decideChunkRecovery(input) {
|
||||
if (!RECOVERABLE_KINDS.has(input.failureKind)) {
|
||||
@@ -16,13 +24,21 @@ export function decideChunkRecovery(input) {
|
||||
if (!input.manifestLoaded) {
|
||||
return { action: "support", reason: "manifest-unavailable" };
|
||||
}
|
||||
if (input.activeReleaseId === input.currentBuildId) {
|
||||
if (
|
||||
input.activeBuildId === input.currentBuildId &&
|
||||
input.activeReleaseId === input.currentReleaseId
|
||||
) {
|
||||
return { action: "support", reason: "same-release" };
|
||||
}
|
||||
|
||||
const releasePair = `${input.currentBuildId}->${input.activeReleaseId}`;
|
||||
const releasePair =
|
||||
`${input.currentBuildId}/${input.currentReleaseId}` +
|
||||
`->${input.activeBuildId}/${input.activeReleaseId}`;
|
||||
const guard = input.storage.read("CHUNK_RELOAD_GUARD");
|
||||
if (!guard.ok || guard.value === releasePair) {
|
||||
if (!guard.ok) {
|
||||
return { action: "support", reason: "guard-read-failed" };
|
||||
}
|
||||
if (guard.value === releasePair) {
|
||||
return { action: "support", reason: "reload-already-attempted" };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user