chore: initialize from frontend template 4dc033c

This commit is contained in:
DongHyeonka
2026-08-13 18:23:26 +09:00
commit 40107eec84
897 changed files with 234824 additions and 0 deletions
+205
View File
@@ -0,0 +1,205 @@
import { normalizeColorSchemePreference } from "./policies/color-scheme.ts";
import type {
ApplicationApi,
ApplicationFeatureId,
ApplicationFeatureInputs,
ColorSchemePreference,
RenderFailureReport,
RouteChangedReport,
} from "./ports/in/application-api.ts";
import type { ApplicationOutputPorts } from "./ports/out/application-output-ports.ts";
import { decideChunkRecovery } from "./use-cases/decide-chunk-recovery.ts";
export type { ApplicationApi, ApplicationOutputPorts };
/**
* Builds the driving API consumed by inbound adapters. Concrete output ports
* remain inside these closures and are never returned to React.
*/
export function createApplication(
outputPorts: ApplicationOutputPorts,
featureInputs: Readonly<Partial<ApplicationFeatureInputs>> = {},
): ApplicationApi {
const session = Object.freeze({
getSnapshot: () => outputPorts.session.getState(),
subscribe: (listener: () => void) =>
outputPorts.session.subscribe(listener),
beginSignIn: (returnTo?: string) =>
outputPorts.session.beginSignIn(returnTo),
signOut: () => outputPorts.session.signOut(),
recover: () => outputPorts.session.recover(),
});
const preferences = Object.freeze({
getColorScheme(): ColorSchemePreference {
const result = outputPorts.preferences.read("COLOR_SCHEME");
return normalizeColorSchemePreference(
result.ok ? result.value : undefined,
);
},
setColorScheme(preference: ColorSchemePreference) {
const normalized = normalizeColorSchemePreference(preference);
return outputPorts.preferences.write("COLOR_SCHEME", normalized);
},
});
const diagnostics = Object.freeze({
reportRenderFailure(report: RenderFailureReport) {
try {
outputPorts.diagnostics.record({
level: "error",
eventId: "ui.render.failed",
context: {
route_id: report.routeId,
build_id: report.buildId,
component_boundary: report.boundaryName,
},
});
} catch {
// Diagnostics are best-effort and cannot become an application failure.
}
try {
outputPorts.telemetry.emit("ui.render.failed", {
route_id: report.routeId,
build_id: report.buildId,
component_boundary: report.boundaryName,
});
} catch {
// Diagnostics are best-effort and cannot become an application failure.
}
},
reportRouteChanged(report: RouteChangedReport) {
try {
outputPorts.diagnostics.record({
level: "info",
eventId: "route.changed",
context: {
route_id: report.routeId,
build_id: report.buildId,
},
});
} catch {
// Diagnostics are best-effort and cannot become navigation failure.
}
},
});
const runtime = Object.freeze({
async getReleaseSummary() {
const release = await outputPorts.releaseInfo.getCurrent();
return Object.freeze({
buildId: release.buildId,
releaseId: release.releaseId,
configSchemaVersion: release.configSchemaVersion,
...(release.apiContractVersion === undefined
? {}
: { apiContractVersion: release.apiContractVersion }),
...(release.contractSetDigest === undefined
? {}
: { contractSetDigest: release.contractSetDigest }),
});
},
getCapabilitySnapshot() {
return outputPorts.runtimeCapabilities.getSnapshot();
},
});
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 (
current.buildId !== active.buildId ||
current.releaseId !== active.releaseId
) {
const mismatchKind =
current.buildId !== active.buildId
? "BUILD_MISMATCH"
: "RELEASE_MISMATCH";
try {
outputPorts.diagnostics.record({
level: "warn",
eventId: "release.mismatch.detected",
context: {
build_id: current.buildId,
active_release_id: active.releaseId,
mismatch_kind: mismatchKind,
},
});
} catch {
// Recovery remains independent from diagnostics.
}
try {
outputPorts.telemetry.emit("release.mismatch.detected", {
build_id: current.buildId,
active_release_id: active.releaseId,
mismatch_kind: mismatchKind,
});
} catch {
// Recovery remains independent from telemetry.
}
}
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",
};
}
},
});
const installedFeatureInputs = Object.freeze({ ...featureInputs });
const features = Object.freeze({
has(featureId: string): featureId is ApplicationFeatureId {
return Object.hasOwn(installedFeatureInputs, featureId);
},
get<FeatureId extends ApplicationFeatureId>(
featureId: FeatureId,
): ApplicationFeatureInputs[FeatureId] {
if (!Object.hasOwn(installedFeatureInputs, featureId)) {
throw new Error(`Application feature is not installed: ${featureId}`);
}
return installedFeatureInputs[
featureId
] as ApplicationFeatureInputs[FeatureId];
},
});
return Object.freeze({
session,
preferences,
diagnostics,
runtime,
recovery,
features,
});
}
+159
View File
@@ -0,0 +1,159 @@
export const BOUNDED_POLLING_CEILINGS = Object.freeze({
minimumIntervalMs: 5_000,
maxIntervalMs: 60_000,
maxAttempts: 120,
maxElapsedMs: 30 * 60 * 1_000,
maxResponseBytes: 8 * 1_024 * 1_024,
maxTerminalStates: 32,
});
export type PollFallbackReason =
| "CONVERGENCE"
| "RELAXED_FRESHNESS"
| "STREAM_DEGRADED"
| "STREAM_UNAVAILABLE";
export type PollLeasePolicy = Readonly<{
operationId: string;
owner: string;
minimumIntervalMs: number;
successIntervalMs: number;
maxIntervalMs: number;
maxAttempts: number;
maxElapsedMs: number;
maxResponseBytes: number;
visibility: "VISIBLE_ONLY";
fallbackReason: PollFallbackReason;
terminalStates: readonly string[];
}>;
/**
* The HTTP executor used by a poll lease must represent exactly one physical
* request. Transport retry, credential replay and cumulative retry sleep stay
* disabled so a poll attempt cannot hide additional requests.
*/
export type BoundedPollOperationContract = Readonly<{
operationId: string;
contractVersion: 2;
protocol: "REST";
semantics: "QUERY";
method: "GET" | "HEAD";
replayPolicy: "SAFE" | "IDEMPOTENT";
retry: "never";
maxResponseBytes: number;
transportMaxAttempts: 1;
authRecoveryCount: 0;
maxCumulativeSleepMs: 0;
serverStream: false;
}>;
const OPERATION_ID = /^[A-Z][A-Z0-9_]{2,79}$/u;
const OWNER_ID = /^[A-Za-z][A-Za-z0-9._:-]{0,127}$/u;
const TERMINAL_STATE = /^[A-Z][A-Z0-9_]{0,63}$/u;
export function definePollLeasePolicy(
input: PollLeasePolicy,
): PollLeasePolicy {
if (
!input ||
typeof input !== "object" ||
!OPERATION_ID.test(input.operationId) ||
!OWNER_ID.test(input.owner) ||
input.visibility !== "VISIBLE_ONLY" ||
![
"CONVERGENCE",
"RELAXED_FRESHNESS",
"STREAM_DEGRADED",
"STREAM_UNAVAILABLE",
].includes(input.fallbackReason) ||
!isIntegerWithin(
input.minimumIntervalMs,
BOUNDED_POLLING_CEILINGS.minimumIntervalMs,
BOUNDED_POLLING_CEILINGS.maxIntervalMs,
) ||
!isIntegerWithin(
input.successIntervalMs,
input.minimumIntervalMs,
BOUNDED_POLLING_CEILINGS.maxIntervalMs,
) ||
!isIntegerWithin(
input.maxIntervalMs,
input.successIntervalMs,
BOUNDED_POLLING_CEILINGS.maxIntervalMs,
) ||
!isIntegerWithin(
input.maxAttempts,
1,
BOUNDED_POLLING_CEILINGS.maxAttempts,
) ||
!isIntegerWithin(
input.maxElapsedMs,
input.minimumIntervalMs,
BOUNDED_POLLING_CEILINGS.maxElapsedMs,
) ||
!isIntegerWithin(
input.maxResponseBytes,
1,
BOUNDED_POLLING_CEILINGS.maxResponseBytes,
) ||
!Array.isArray(input.terminalStates) ||
input.terminalStates.length >
BOUNDED_POLLING_CEILINGS.maxTerminalStates ||
input.terminalStates.some(
(state) =>
typeof state !== "string" || !TERMINAL_STATE.test(state),
) ||
new Set(input.terminalStates).size !== input.terminalStates.length ||
(input.fallbackReason === "CONVERGENCE" &&
input.terminalStates.length === 0)
) {
throw new TypeError("Invalid bounded polling lease policy.");
}
return Object.freeze({
...input,
terminalStates: Object.freeze([...input.terminalStates]),
});
}
export function assertBoundedPollOperation(
policy: PollLeasePolicy,
operation: BoundedPollOperationContract,
): void {
if (
!operation ||
typeof operation !== "object" ||
operation.operationId !== policy.operationId ||
operation.contractVersion !== 2 ||
operation.protocol !== "REST" ||
operation.semantics !== "QUERY" ||
!["GET", "HEAD"].includes(operation.method) ||
!["SAFE", "IDEMPOTENT"].includes(operation.replayPolicy) ||
operation.retry !== "never" ||
operation.serverStream !== false ||
operation.transportMaxAttempts !== 1 ||
operation.authRecoveryCount !== 0 ||
operation.maxCumulativeSleepMs !== 0 ||
!isIntegerWithin(
operation.maxResponseBytes,
1,
BOUNDED_POLLING_CEILINGS.maxResponseBytes,
)
) {
throw new TypeError(
"Bounded polling operation must be one terminal replay-safe REST request.",
);
}
}
function isIntegerWithin(
value: number,
minimum: number,
maximum: number,
): boolean {
return (
Number.isSafeInteger(value) &&
value >= minimum &&
value <= maximum
);
}
+26
View File
@@ -0,0 +1,26 @@
export const COLOR_SCHEME_PREFERENCES = Object.freeze([
"system",
"light",
"dark",
] as const);
export type ColorSchemePreference =
(typeof COLOR_SCHEME_PREFERENCES)[number];
export type ResolvedColorScheme = Exclude<ColorSchemePreference, "system">;
export function normalizeColorSchemePreference(
value: unknown,
): ColorSchemePreference {
return typeof value === "string" &&
COLOR_SCHEME_PREFERENCES.some((preference) => preference === value)
? (value as ColorSchemePreference)
: "system";
}
export function resolveColorScheme(
preference: ColorSchemePreference,
systemPrefersDark: boolean,
): ResolvedColorScheme {
if (preference === "system") return systemPrefersDark ? "dark" : "light";
return preference;
}
+107
View File
@@ -0,0 +1,107 @@
export const COMPATIBILITY_TUPLE_FIELDS = Object.freeze([
"buildId",
"configSchemaVersion",
"apiContractVersion",
"assetManifestHash",
"releaseId",
] as const);
export type CompatibilityTupleField =
(typeof COMPATIBILITY_TUPLE_FIELDS)[number];
export type CompatibilityTuple = Readonly<
Record<CompatibilityTupleField, string>
>;
export type NumericVersion = Readonly<{
major: number;
minor: number;
patch: number;
}>;
export function parseNumericVersion(version: string): NumericVersion | null {
const match = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(version);
if (!match) return null;
return {
major: Number(match[1]),
minor: Number(match[2] ?? 0),
patch: Number(match[3] ?? 0),
};
}
export function isVersionCompatible(
supported: string,
actual: string,
): boolean {
const expected = parseNumericVersion(supported);
const candidate = parseNumericVersion(actual);
if (!expected || !candidate) return false;
return (
expected.major === candidate.major &&
candidate.minor >= expected.minor
);
}
export function verifyCompatibilityTuple(input: Readonly<{
frontend: CompatibilityTuple;
runtime: CompatibilityTuple;
}>) {
const mismatches: CompatibilityTupleField[] = [];
if (input.frontend.buildId !== input.runtime.buildId) mismatches.push("buildId");
if (
!isVersionCompatible(
input.frontend.configSchemaVersion,
input.runtime.configSchemaVersion,
)
) {
mismatches.push("configSchemaVersion");
}
if (
!isVersionCompatible(
input.frontend.apiContractVersion,
input.runtime.apiContractVersion,
)
) {
mismatches.push("apiContractVersion");
}
if (input.frontend.assetManifestHash !== input.runtime.assetManifestHash) {
mismatches.push("assetManifestHash");
}
const releaseWarning: "releaseId" | null =
input.frontend.releaseId === input.runtime.releaseId
? null
: "releaseId";
return Object.freeze({
compatible: mismatches.length === 0,
mismatches: Object.freeze(mismatches),
warnings: Object.freeze(releaseWarning ? [releaseWarning] : []),
});
}
export type ObjectSchemaShape = Readonly<{
required?: readonly string[];
properties?: Readonly<Record<string, unknown>>;
}>;
export type SchemaChangeClassification = "breaking" | "additive" | "none";
export function classifyObjectSchemaChange(
before: ObjectSchemaShape,
after: ObjectSchemaShape,
): SchemaChangeClassification {
const beforeRequired = new Set(before.required ?? []);
const afterRequired = new Set(after.required ?? []);
const removedProperties = Object.keys(before.properties ?? {}).filter(
(key) => !(key in (after.properties ?? {})),
);
const addedRequired = [...afterRequired].filter(
(key) => !beforeRequired.has(key),
);
if (removedProperties.length > 0 || addedRequired.length > 0) return "breaking";
const addedProperties = Object.keys(after.properties ?? {}).filter(
(key) => !(key in (before.properties ?? {})),
);
return addedProperties.length > 0 ? "additive" : "none";
}
@@ -0,0 +1,118 @@
export type BundleMeasurements = Readonly<{
initialJsGzipBytes: number;
lazyChunks: readonly Readonly<{ path: string; gzipBytes: number }>[];
}>;
export type BundleThresholds = Readonly<{
initialJsGzipBytes: number;
lazyChunkGzipBytes: number;
}>;
export function evaluateBundleBudget(
measurements: BundleMeasurements,
thresholds: BundleThresholds,
) {
const initialPassed =
measurements.initialJsGzipBytes <= thresholds.initialJsGzipBytes;
const lazyResults = measurements.lazyChunks.map((chunk) => ({
...chunk,
threshold: thresholds.lazyChunkGzipBytes,
passed: chunk.gzipBytes <= thresholds.lazyChunkGzipBytes,
}));
return Object.freeze({
initialPassed,
lazyResults: Object.freeze(lazyResults),
passed: initialPassed && lazyResults.every((chunk) => chunk.passed),
});
}
export type LabMetrics = Readonly<{
lcpMs: number;
cls: number;
namedInteractionMs: number;
}>;
export function evaluateLabBudget(
report: Readonly<{
context?: Readonly<Record<string, unknown>>;
metrics: LabMetrics;
}>,
thresholds: LabMetrics,
) {
const requiredContext = [
"runner",
"browser",
"viewport",
"network",
"cpu",
"cache",
"build",
];
const missingContext = requiredContext.filter(
(field) => report.context?.[field] === undefined,
);
const results = {
lcp: report.metrics.lcpMs <= thresholds.lcpMs,
cls: report.metrics.cls <= thresholds.cls,
namedInteraction:
report.metrics.namedInteractionMs <= thresholds.namedInteractionMs,
};
return Object.freeze({
missingContext: Object.freeze(missingContext),
results: Object.freeze(results),
passed: missingContext.length === 0 && Object.values(results).every(Boolean),
});
}
export function percentile75(values: readonly number[]): number | null {
if (values.length === 0) return null;
const sorted = [...values].sort((left, right) => left - right);
return sorted[Math.ceil(sorted.length * 0.75) - 1];
}
export type FieldMetrics = Readonly<{
p75LcpMs: number | null;
p75Cls: number | null;
p75InpMs: number | null;
}>;
export type FieldThresholds = Readonly<{
p75LcpMs: number;
p75Cls: number;
p75InpMs: number;
minimumEligibleSamples: number | null;
}>;
export type FieldBudgetResult = Readonly<{
status: "PASS" | "FAIL_THRESHOLD" | "FAIL_UNVERIFIED";
passed: boolean;
}>;
export function evaluateFieldBudget(
report: Readonly<{ metrics: FieldMetrics; eligibleSamples: number }>,
thresholds: FieldThresholds,
): FieldBudgetResult {
if (
thresholds.minimumEligibleSamples === null ||
report.eligibleSamples < thresholds.minimumEligibleSamples ||
Object.values(report.metrics).some((value) => value === null)
) {
return Object.freeze({
status: "FAIL_UNVERIFIED",
passed: false,
});
}
const metrics = report.metrics as Readonly<{
p75LcpMs: number;
p75Cls: number;
p75InpMs: number;
}>;
const passed =
metrics.p75LcpMs <= thresholds.p75LcpMs &&
metrics.p75Cls <= thresholds.p75Cls &&
metrics.p75InpMs <= thresholds.p75InpMs;
return Object.freeze({
status: passed ? "PASS" : "FAIL_THRESHOLD",
passed,
});
}
@@ -0,0 +1,60 @@
export const PROMOTION_FORMULA = Object.freeze({
MERGE_READY: Object.freeze([
"FE-GATE-001",
"FE-GATE-002",
"FE-GATE-003",
"FE-GATE-004",
"FE-GATE-005",
"FE-GATE-006",
"FE-GATE-007",
"FE-GATE-008",
"FE-GATE-009",
"FE-GATE-010",
"FE-GATE-011",
"FE-GATE-013",
"FE-GATE-020",
]),
RELEASE_READY: Object.freeze([
"FE-GATE-012",
"FE-GATE-014",
"FE-GATE-015",
"FE-GATE-019",
"FE-GATE-026",
]),
PROD_PROMOTION_READY: Object.freeze([
"FE-GATE-016",
"FE-GATE-021",
"FE-GATE-022",
"FE-GATE-023",
"FE-GATE-024",
"FE-GATE-025",
]),
FIELD_SLO_READY: Object.freeze(["FE-GATE-018"]),
DOCUMENTATION_READY: Object.freeze(["FE-GATE-017"]),
});
export type GateResult = "PASS" | "FAIL" | "UNVERIFIED";
export function evaluatePromotionReadiness(
gateResults: Readonly<Record<string, GateResult>>,
) {
const allPass = (gateIds: readonly string[]) =>
gateIds.every((gateId) => gateResults[gateId] === "PASS");
const mergeReady = allPass(PROMOTION_FORMULA.MERGE_READY);
const releaseReady =
mergeReady && allPass(PROMOTION_FORMULA.RELEASE_READY);
const productionReady =
releaseReady && allPass(PROMOTION_FORMULA.PROD_PROMOTION_READY);
const fieldReady =
productionReady && allPass(PROMOTION_FORMULA.FIELD_SLO_READY);
const documentationReady = allPass(PROMOTION_FORMULA.DOCUMENTATION_READY);
return Object.freeze({
MERGE_READY: mergeReady,
RELEASE_READY: releaseReady,
PROD_PROMOTION_READY: productionReady,
FIELD_SLO_READY: fieldReady,
DOCUMENTATION_READY: documentationReady,
});
}
@@ -0,0 +1,30 @@
export type SessionState =
| "authenticated"
| "unauthenticated"
| "recovery-pending"
| "integration-failed";
export type SessionGateway = Readonly<{
getState(): SessionState;
subscribe(listener: () => void): () => void;
beginSignIn(returnTo?: string): Promise<void>;
signOut(): Promise<void>;
recover(): Promise<"restored" | "no-session">;
}>;
export type CredentialRequestBinding = Readonly<{
origin: string;
method: string;
operationId: string;
}>;
export type CredentialPatch = Readonly<{
headers: Readonly<Record<string, string>>;
}>;
export type CredentialAttacher = Readonly<{
credentialPatch(binding: CredentialRequestBinding): Promise<CredentialPatch>;
onUnauthenticated(): void;
}>;
export type AuthSessionPort = SessionGateway & CredentialAttacher;
@@ -0,0 +1,90 @@
import type {
BrowserDataResult,
ByteSource,
} from "./shared.ts";
export type PublicCacheHeader = readonly [name: string, value: string];
export type PublicCacheAsset = Readonly<{
absoluteUrl: string;
expectedByteLength: number;
expectedContentType: string;
integrity: Readonly<{
algorithm: "SHA-256";
digestHex: string;
}>;
requestHeaders?: readonly PublicCacheHeader[];
}>;
export type PublicCacheReleaseManifest = Readonly<{
releaseRegistryId: string;
manifestDigestHex: string;
assets: readonly PublicCacheAsset[];
}>;
export type PublicCacheReleaseSummary = Readonly<{
releaseRegistryId: string;
entryCount: number;
totalBytes: number;
stagedAtEpochMs: number;
}>;
export type CachedPublicResponse = Readonly<{
status: 200;
headers: readonly PublicCacheHeader[];
body: ByteSource;
integrity: Readonly<{
algorithm: "SHA-256";
digestHex: string;
}>;
}>;
export type PublicCacheInspection = Readonly<{
activeReleaseRegistryId: string | null;
ownedCacheCount: number;
unreadableOwnedCacheCount: number;
releaseCandidates: readonly Readonly<{
releaseRegistryId: string;
verified: boolean;
entryCount: number | null;
}>[];
}>;
export type PublicCacheCleanupReport = Readonly<{
inspectedOwnedCaches: number;
deletedOwnedCaches: number;
retainedOwnedCaches: number;
}>;
export interface PublicResponseCachePort {
matchActiveExact(
request: Readonly<{
absoluteUrl: string;
requestHeaders?: readonly PublicCacheHeader[];
signal?: AbortSignal;
}>,
): Promise<BrowserDataResult<CachedPublicResponse | null>>;
}
export interface PublicResponseCacheAdminPort {
stageRelease(
manifest: PublicCacheReleaseManifest,
options?: Readonly<{ signal?: AbortSignal }>,
): Promise<BrowserDataResult<PublicCacheReleaseSummary>>;
activateRelease(
releaseRegistryId: string,
manifestDigestHex: string,
options?: Readonly<{ signal?: AbortSignal }>,
): Promise<BrowserDataResult<PublicCacheReleaseSummary>>;
cleanupOwned(
request?: Readonly<{
signal?: AbortSignal;
}>,
): Promise<BrowserDataResult<PublicCacheCleanupReport>>;
inspect(): Promise<BrowserDataResult<PublicCacheInspection>>;
}
export type PublicResponseCache = Readonly<{
responses: PublicResponseCachePort;
admin: PublicResponseCacheAdminPort;
}>;
@@ -0,0 +1,250 @@
import type {
BrowserDataResult,
TransferProgress,
} from "./shared.ts";
import type { AuthorizedDownloadCapability } from "../browser-transfer/authorized-download.ts";
/**
* Application-owned browser-file contracts.
*
* Native File, Blob, FileList, FileSystemHandle, Response and ReadableStream
* intentionally do not cross this boundary. A transient object URL may cross
* only through the presentation-local PreviewLease below; it must never enter
* domain state, persistence, diagnostics or a general application cache.
*/
declare const localFileRefBrand: unique symbol;
declare const fileVerificationReceiptBrand: unique symbol;
declare const filePolicyKeyBrand: unique symbol;
declare const filePolicyIntentionBrand: unique symbol;
declare const browserManagedCapabilityReceiptBrand: unique symbol;
export type LocalFileRef = string & {
readonly [localFileRefBrand]: "LocalFileRef";
};
export type FileVerificationReceipt = string & {
readonly [fileVerificationReceiptBrand]: "FileVerificationReceipt";
};
/**
* Registry-issued, non-semantic identifiers. A feature receives a frozen
* reference from its composition root; presentation must not construct policy
* definitions or select another feature's registered policy.
*/
export type FilePolicyKey = string & {
readonly [filePolicyKeyBrand]: "FilePolicyKey";
};
export type FilePolicyIntention = string & {
readonly [filePolicyIntentionBrand]: "FilePolicyIntention";
};
export type FilePolicyReference = Readonly<{
policyKey: FilePolicyKey;
intention: FilePolicyIntention;
}>;
export type FileSelectionSource =
| "NATIVE_INPUT"
| "SYSTEM_PICKER"
| "DROP";
export type FileSelectionLimitReduction = Readonly<{
maxCount?: number;
maxFileBytes?: number;
maxTotalBytes?: number;
}>;
export type FileCandidate = Readonly<{
ref: LocalFileRef;
/**
* Untrusted, potentially personal display metadata. It must never be used as
* a resource identifier or diagnostics attribute.
*/
displayName: string;
sizeBytes: number;
reportedMediaType: string | null;
lastModifiedEpochMs: number | null;
source: FileSelectionSource;
}>;
export type FileSelectionOutcome =
| Readonly<{ kind: "SELECTED"; files: readonly FileCandidate[] }>
| Readonly<{ kind: "DISMISSED" }>;
export type FilePickerSupport = Readonly<{
nativeInput: true;
systemOpenPicker: boolean;
systemSavePicker: boolean;
}>;
export interface FilePickerPort {
readonly support: FilePickerSupport;
/**
* Must be invoked as the first browser action in a trusted user activation.
* A dismissed picker is a successful DISMISSED outcome, not an error.
*/
select(input: {
policy: FilePolicyReference;
limits?: FileSelectionLimitReduction;
signal?: AbortSignal;
}): Promise<BrowserDataResult<FileSelectionOutcome>>;
release(ref: LocalFileRef): void;
}
export type FileSignatureResult =
| "MATCHED"
| "MISMATCHED"
| "UNKNOWN";
export type FileInspection = Readonly<{
byteLength: number;
reportedMediaType: string | null;
detectedMediaType: string | null;
normalizedExtension: string | null;
signature: FileSignatureResult;
/**
* Issued only for a matched signature and bound inside the transient vault
* to this file snapshot and inspection policy.
*/
verificationReceipt: FileVerificationReceipt | null;
}>;
/**
* File-capability byte stream with a closed failure channel. Implementations
* must convert native exceptions to BrowserDataResult and never throw a raw
* DOMException across the application boundary.
*/
export interface FileByteSource {
readonly byteLength: number | null;
stream(
signal: AbortSignal,
): AsyncIterable<BrowserDataResult<Uint8Array>>;
}
export interface FileContentPort {
inspect(input: {
ref: LocalFileRef;
policy: FilePolicyReference;
maxInspectionBytes?: number;
signal: AbortSignal;
}): Promise<BrowserDataResult<FileInspection>>;
readRange(input: {
ref: LocalFileRef;
offset: number;
length: number;
signal: AbortSignal;
}): Promise<BrowserDataResult<Uint8Array>>;
openSource(input: {
ref: LocalFileRef;
signal: AbortSignal;
}): Promise<BrowserDataResult<FileByteSource>>;
release(ref: LocalFileRef): void;
}
export type PreviewLease = Readonly<{
url: string;
mediaType: string;
release(): void;
}>;
export interface TransientPreviewPort {
create(input: {
ref: LocalFileRef;
verificationReceipt: FileVerificationReceipt;
policy: FilePolicyReference;
maxPreviewBytes?: number;
signal: AbortSignal;
}): Promise<BrowserDataResult<PreviewLease>>;
dispose(): void;
}
export type BrowserManagedDownloadCapabilityReceipt = string & {
readonly [browserManagedCapabilityReceiptBrand]:
"BrowserManagedDownloadCapabilityReceipt";
};
export type DownloadSource =
| Readonly<{
kind: "BROWSER_MANAGED_RESOURCE";
resourceId: string;
capabilityReceipt: BrowserManagedDownloadCapabilityReceipt;
}>
| Readonly<{
kind: "AUTHORIZED_STREAM_RESOURCE";
resourceId: string;
/**
* Exact provider-issued handle. Raw href/query/header values are never
* caller inputs and an equal-looking fabricated handle must be rejected.
*/
capability: AuthorizedDownloadCapability;
}>
| Readonly<{
kind: "GENERATED";
bytes: FileByteSource;
expectedSha256?: string;
}>;
export type DownloadStrategy =
| "BROWSER_MANAGED"
| "PROMPT_AND_STREAM"
| "BOUNDED_OBJECT_URL";
export type DownloadOutcome =
| Readonly<{
kind: "BROWSER_HANDOFF";
transferId: string;
}>
| Readonly<{
kind: "SAVED";
transferId: string;
bytesWritten: number;
integrity: "VERIFIED" | "NOT_PROVIDED";
}>
| Readonly<{ kind: "DISMISSED" }>;
export interface DownloadDeliveryPort {
deliver(input: {
policy: FilePolicyReference;
source: DownloadSource;
suggestedFileName: string;
/**
* Optional reductions of the composition-owned policy ceiling. These
* values can never raise the registered or absolute runtime limits.
*/
maxTransferBytes?: number;
maxBufferedBytes?: number;
signal: AbortSignal;
onProgress(progress: TransferProgress): void;
}): Promise<BrowserDataResult<DownloadOutcome>>;
}
/**
* Synchronously resolved, server-enforced handoff capability. The endpoint
* behind href must bind and enforce every field, including expiry and the
* optional digest; the browser adapter cannot observe navigation bytes.
*/
export type BrowserManagedDownloadCapability = Readonly<{
capabilityReceipt: BrowserManagedDownloadCapabilityReceipt;
href: string;
resourceId: string;
mediaType: string;
safeExtension: string;
maxBytes: number;
expectedSha256?: string;
expiresAtEpochMs: number;
}>;
export interface BrowserManagedDownloadCapabilityResolver {
resolve(input: Readonly<{
resourceId: string;
capabilityReceipt: BrowserManagedDownloadCapabilityReceipt;
}>): BrowserDataResult<BrowserManagedDownloadCapability>;
}
@@ -0,0 +1,114 @@
export type {
BrowserDataFailure,
BrowserDataFailureCode,
BrowserAccountDeletionAction,
BrowserDataAuthority,
BrowserAccountScope,
BrowserDataObservation,
BrowserDataObserver,
BrowserLogoutAction,
BrowserDataOperation,
BrowserPressureAction,
BrowserDataRecovery,
BrowserDataResult,
BrowserStoragePolicy,
ByteSource,
PersistableDataClass,
TransferProgress,
} from "./shared.ts";
export {
assertValidStoragePolicy,
isValidByteLength,
} from "./shared.ts";
export type {
BrowserManagedDownloadCapability,
BrowserManagedDownloadCapabilityReceipt,
BrowserManagedDownloadCapabilityResolver,
DownloadDeliveryPort,
DownloadOutcome,
DownloadSource,
DownloadStrategy,
FileByteSource,
FileCandidate,
FileContentPort,
FileInspection,
FilePolicyIntention,
FilePolicyKey,
FilePolicyReference,
FilePickerPort,
FilePickerSupport,
FileSelectionOutcome,
FileSelectionLimitReduction,
FileSelectionSource,
FileSignatureResult,
FileVerificationReceipt,
LocalFileRef,
PreviewLease,
TransientPreviewPort,
} from "./file.ts";
export type {
IndexedDbCompareAndSwapInput,
IndexedDbConnectionStatus,
IndexedDbCursor,
IndexedDbCursorKey,
IndexedDbDatasetScope,
IndexedDbDeleteInput,
IndexedDbLifecycleAction,
IndexedDbLifecycleAuthorityDecision,
IndexedDbLifecycleAuthorityRequest,
IndexedDbLifecycleBatchInput,
IndexedDbLifecycleBatchReceipt,
IndexedDbMaintenanceBatchInput,
IndexedDbMaintenanceBatchReceipt,
IndexedDbMaintenancePort,
IndexedDbPage,
IndexedDbReceiptPruneBatchReceipt,
IndexedDbRepositoryPort,
IndexedDbSynchronizationState,
IndexedDbWriteReceipt,
} from "./indexeddb-port.ts";
export type {
BeginOpfsJournalTransaction,
DurableObjectDescriptor,
DurableObjectMaintenancePort,
DurableObjectStorePort,
OpenDurableObjectRequest,
OpenedDurableObject,
OpfsCapabilities,
OpfsChunkReference,
OpfsCommittedObjectPage,
OpfsIntegrity,
OpfsJournalMutation,
OpfsJournalPage,
OpfsJournalPhase,
OpfsJournalPort,
OpfsJournalTransaction,
OpfsPolicyMaintenanceReport,
OpfsPreparedObject,
OpfsReconciliationReport,
OpfsSensitiveMaintenanceReason,
OpfsStorageScope,
PutDurableObjectRequest,
RemoveDurableObjectRequest,
} from "./opfs-ports.ts";
export type {
CachedPublicResponse,
PublicCacheAsset,
PublicCacheCleanupReport,
PublicCacheHeader,
PublicCacheInspection,
PublicCacheReleaseManifest,
PublicCacheReleaseSummary,
PublicResponseCache,
PublicResponseCacheAdminPort,
PublicResponseCachePort,
} from "./cache-storage-ports.ts";
export type {
StorageDurabilityPort,
StorageEstimate,
} from "./storage-durability-port.ts";
@@ -0,0 +1,186 @@
import type {
BrowserAccountScope,
BrowserDataResult,
BrowserStoragePolicy,
} from "./shared.ts";
/**
* Registry-issued, non-semantic dataset identity. Tokens must be random and
* must never contain a tenant, account, user, email, domain object ID, or the
* human-readable policy namespace.
*/
export type IndexedDbDatasetScope = Readonly<{
authorityToken: string;
namespaceToken: string;
partitionToken: string;
accountScope: BrowserAccountScope;
}>;
export type IndexedDbSynchronizationState =
| "PENDING"
| "CONFIRMED";
export type IndexedDbConnectionStatus =
| Readonly<{ kind: "CLOSED"; reason: "NOT_OPENED" | "VERSION_CHANGE" | "FORCED" }>
| Readonly<{ kind: "OPENING"; targetVersion: number }>
| Readonly<{
kind: "BLOCKED";
currentVersion: number;
targetVersion: number;
}>
| Readonly<{ kind: "READY"; schemaVersion: number }>
| Readonly<{ kind: "DISPOSED" }>;
export type IndexedDbCursorKey =
| string
| number
| Date
| ArrayBuffer
| readonly IndexedDbCursorKey[];
/**
* Opaque continuation state owned by an adapter query policy. Feature ports
* should wrap this value if a cursor crosses a presentation or URL boundary.
*/
export type IndexedDbCursor = Readonly<{
indexKey: IndexedDbCursorKey;
primaryKey: IndexedDbCursorKey;
}>;
export type IndexedDbPage<Value> = Readonly<{
items: readonly Value[];
nextCursor: IndexedDbCursor | null;
}>;
export type IndexedDbWriteReceipt = Readonly<{
key: string;
revision: number;
replayed: boolean;
}>;
export type IndexedDbCompareAndSwapInput<Value> = Readonly<{
key: string;
value: Value;
expectedRevision: number | null;
idempotencyKey: string;
/**
* Required only for UNTIL_SYNCED datasets. The adapter never infers server
* acknowledgement from a successful local write.
*/
synchronization?: IndexedDbSynchronizationState;
signal?: AbortSignal;
}>;
export type IndexedDbDeleteInput = Readonly<{
key: string;
expectedRevision: number;
idempotencyKey: string;
signal?: AbortSignal;
}>;
export type IndexedDbMaintenanceBatchInput = Readonly<{
/**
* Hard row-count ceiling for one invocation. The adapter also observes the
* cooperative duration budget between asynchronous storage operations.
*/
maxRows: number;
maxDurationMs: number;
signal?: AbortSignal;
}>;
export type IndexedDbMaintenanceBatchReceipt = Readonly<{
state: "MORE" | "COMPLETE";
scannedRows: number;
checkpointedRows: number;
migratedRows: number;
concurrentlyChangedRows: number;
budgetExhausted: boolean;
}>;
export type IndexedDbReceiptPruneBatchReceipt = Readonly<{
state: "MORE" | "COMPLETE";
scannedRows: number;
deletedRows: number;
budgetExhausted: boolean;
}>;
export type IndexedDbLifecycleAction =
| "SESSION_END"
| "LOGOUT"
| "ACCOUNT_DELETION"
| "RETENTION_SWEEP";
export type IndexedDbLifecycleBatchInput = Readonly<{
action: IndexedDbLifecycleAction;
maxRows: number;
maxDurationMs: number;
signal?: AbortSignal;
}>;
export type IndexedDbLifecycleBatchReceipt = Readonly<{
state: "MORE" | "COMPLETE";
scannedRows: number;
deletedRows: number;
budgetExhausted: boolean;
}>;
export type IndexedDbLifecycleAuthorityRequest = Readonly<{
action: IndexedDbLifecycleAction;
scope: IndexedDbDatasetScope;
storagePolicy: BrowserStoragePolicy;
signal?: AbortSignal;
}>;
export type IndexedDbLifecycleAuthorityDecision =
| Readonly<{ authorized: false }>
| Readonly<{
authorized: true;
/** Opaque, short-lived proof. It is validated and discarded, never stored. */
proofToken: string;
}>;
/**
* Domain-neutral asynchronous repository boundary. Native IndexedDB objects,
* object-store names, indexes and transaction callbacks remain adapter-local.
*/
export interface IndexedDbRepositoryPort<Value, Query> {
open(signal?: AbortSignal): Promise<BrowserDataResult<void>>;
read(
key: string,
signal?: AbortSignal,
): Promise<BrowserDataResult<Readonly<{ value: Value; revision: number }> | null>>;
query(
query: Query,
cursor?: IndexedDbCursor | null,
signal?: AbortSignal,
): Promise<BrowserDataResult<IndexedDbPage<Value>>>;
compareAndSwap(
input: IndexedDbCompareAndSwapInput<Value>,
): Promise<BrowserDataResult<IndexedDbWriteReceipt>>;
remove(
input: IndexedDbDeleteInput,
): Promise<BrowserDataResult<IndexedDbWriteReceipt>>;
/**
* Bounded destructive lifecycle work. Every deleting invocation is gated by
* the composition-root authority callback; callers cannot provide proof.
*/
enforceLifecycleBatch(
input: IndexedDbLifecycleBatchInput,
): Promise<BrowserDataResult<IndexedDbLifecycleBatchReceipt>>;
getStatus(): IndexedDbConnectionStatus;
subscribeStatus(listener: (status: IndexedDbConnectionStatus) => void): () => void;
close(): void;
}
/**
* Bounded, restart-safe maintenance boundary. Checkpoint keys and raw stored
* records stay private to the adapter; callers receive aggregate progress only.
*/
export interface IndexedDbMaintenancePort {
migrateCodecBatch(
input: IndexedDbMaintenanceBatchInput,
): Promise<BrowserDataResult<IndexedDbMaintenanceBatchReceipt>>;
pruneExpiredReceipts(
input: IndexedDbMaintenanceBatchInput,
): Promise<BrowserDataResult<IndexedDbReceiptPruneBatchReceipt>>;
}
@@ -0,0 +1,276 @@
import type {
BrowserDataResult,
BrowserStoragePolicy,
ByteSource,
TransferProgress,
} from "./shared.ts";
export type OpfsIntegrity = Readonly<{
algorithm: "SHA-256-TREE-V1";
rootDigestHex: string;
chunkSizeBytes: number;
}>;
/**
* namespaceToken and partitionToken must be random/opaque registry values.
* Raw account IDs, email addresses and business identifiers are forbidden.
* Only these tokens, never namespace or owner, may be used in physical paths.
*/
export type OpfsStorageScope = Readonly<{
namespace: string;
authorityToken: string;
namespaceToken: string;
partitionToken: string;
}>;
export type DurableObjectDescriptor = Readonly<{
objectId: string;
scope: OpfsStorageScope;
generation: number;
byteLength: number;
mediaType: string;
createdAtEpochMs: number;
integrity: OpfsIntegrity;
storagePolicy: BrowserStoragePolicy;
}>;
export type PutDurableObjectRequest = Readonly<{
objectId: string;
expectedGeneration: number | null;
mediaType: string;
source: ByteSource;
signal?: AbortSignal;
onProgress?: (progress: TransferProgress) => void;
}>;
export type OpenDurableObjectRequest = Readonly<{
objectId: string;
generation?: number;
signal?: AbortSignal;
}>;
export type RemoveDurableObjectRequest = Readonly<{
objectId: string;
expectedGeneration: number;
signal?: AbortSignal;
}>;
export type OpenedDurableObject = Readonly<{
descriptor: DurableObjectDescriptor;
source: ByteSource;
}>;
export type OpfsCapabilities = Readonly<{
available: boolean;
dedicatedWorkerRequired: true;
crossContextMutationLockAvailable: boolean;
synchronousAccessHandleAvailable: boolean;
}>;
export interface DurableObjectStorePort {
capabilities(): Promise<BrowserDataResult<OpfsCapabilities>>;
put(
request: PutDurableObjectRequest,
): Promise<BrowserDataResult<DurableObjectDescriptor>>;
open(
request: OpenDurableObjectRequest,
): Promise<BrowserDataResult<OpenedDurableObject>>;
remove(
request: RemoveDurableObjectRequest,
): Promise<BrowserDataResult<void>>;
}
export type OpfsReconciliationReport = Readonly<{
inspectedTransactions: number;
committedTransactions: number;
rolledBackTransactions: number;
cleanedTransactions: number;
inspectedOrphanChunks: number;
deletedOrphanChunks: number;
orphanGcStatus:
| "COMPLETED"
| "DEADLINE_REACHED"
| "STAGING_STATE_UNREADABLE";
moreTransactionsAvailable: boolean;
deadlineReached: boolean;
}>;
export type OpfsPolicyMaintenanceReport = Readonly<{
inspectedObjects: number;
removedObjects: number;
releasedBytes: number;
moreObjectsAvailable: boolean;
deadlineReached: boolean;
}>;
export type OpfsSensitiveMaintenanceReason =
| "LOGOUT"
| "UNTIL_SYNCED"
| "ACCOUNT_DELETION";
export interface DurableObjectMaintenancePort {
reconcile(
request?: Readonly<{
budgetMs?: number;
maxTransactions?: number;
signal?: AbortSignal;
}>,
): Promise<BrowserDataResult<OpfsReconciliationReport>>;
enforcePolicies(
request:
| Readonly<{
reason: "TTL";
budgetMs?: number;
maxObjects?: number;
signal?: AbortSignal;
}>
| Readonly<{
reason: "LOGOUT";
budgetMs?: number;
maxObjects?: number;
signal?: AbortSignal;
}>
| Readonly<{
reason: "SESSION_END";
budgetMs?: number;
maxObjects?: number;
signal?: AbortSignal;
}>
| Readonly<{
reason: "UNTIL_SYNCED";
budgetMs?: number;
maxObjects?: number;
signal?: AbortSignal;
}>
| Readonly<{
reason: "ACCOUNT_DELETION";
budgetMs?: number;
maxObjects?: number;
signal?: AbortSignal;
}>
| Readonly<{
reason: "PRESSURE";
targetBytesToRelease: number;
budgetMs?: number;
maxObjects?: number;
signal?: AbortSignal;
}>,
): Promise<BrowserDataResult<OpfsPolicyMaintenanceReport>>;
}
/**
* Physical details are kept in the journal contract, not in the object-store
* API. The IndexedDB implementation must update the journal row and logical
* object row in the same readwrite transaction and re-check the fencing token.
*/
export type OpfsChunkReference = Readonly<{
sequence: number;
byteLength: number;
digestHex: string;
}>;
export type OpfsPreparedObject = Readonly<{
descriptor: DurableObjectDescriptor;
chunks: readonly OpfsChunkReference[];
physicalSchemaVersion: 1;
}>;
export type OpfsJournalMutation = "PUT" | "DELETE";
export type OpfsJournalPhase =
| "PREPARING"
| "FILES_READY"
| "COMMITTED";
export type OpfsJournalTransaction = Readonly<{
transactionId: string;
fencingToken: string;
mutation: OpfsJournalMutation;
scope: OpfsStorageScope;
phase: OpfsJournalPhase;
objectId: string;
expectedGeneration: number | null;
targetGeneration: number;
targetByteLength: number;
targetStoragePolicy: BrowserStoragePolicy;
budgetReservation: Readonly<{
namespace: string;
reservedBytes: number;
hardBudgetBytes: number;
}>;
startedAtEpochMs: number;
preparedObject?: OpfsPreparedObject;
}>;
export type BeginOpfsJournalTransaction = Readonly<{
transactionId: string;
mutation: OpfsJournalMutation;
scope: OpfsStorageScope;
objectId: string;
expectedGeneration: number | null;
targetGeneration: number;
targetByteLength: number;
targetStoragePolicy: BrowserStoragePolicy;
startedAtEpochMs: number;
}>;
export type OpfsJournalPage = Readonly<{
transactions: readonly OpfsJournalTransaction[];
moreAvailable: boolean;
}>;
export type OpfsCommittedObjectPage = Readonly<{
objects: readonly OpfsPreparedObject[];
nextObjectId: string | null;
moreAvailable: boolean;
}>;
export interface OpfsJournalPort {
getCommittedObject(
scope: OpfsStorageScope,
objectId: string,
): Promise<BrowserDataResult<OpfsPreparedObject | null>>;
begin(
transaction: BeginOpfsJournalTransaction,
): Promise<BrowserDataResult<OpfsJournalTransaction>>;
markFilesReady(
transactionId: string,
fencingToken: string,
preparedObject: OpfsPreparedObject,
): Promise<BrowserDataResult<OpfsJournalTransaction>>;
/**
* Atomically publishes preparedObject and advances the journal to COMMITTED.
*/
commitPut(
transactionId: string,
fencingToken: string,
): Promise<BrowserDataResult<OpfsJournalTransaction>>;
/**
* Atomically removes the logical object and advances the journal to COMMITTED.
*/
commitDelete(
transactionId: string,
fencingToken: string,
): Promise<BrowserDataResult<OpfsJournalTransaction>>;
complete(
transactionId: string,
fencingToken: string,
): Promise<BrowserDataResult<void>>;
rollback(
transactionId: string,
fencingToken: string,
): Promise<BrowserDataResult<void>>;
listIncomplete(
limit: number,
): Promise<BrowserDataResult<OpfsJournalPage>>;
listCommittedObjects(
request: Readonly<{
scope: OpfsStorageScope;
afterObjectId?: string;
limit: number;
}>,
): Promise<BrowserDataResult<OpfsCommittedObjectPage>>;
isChunkReferenced(
scope: OpfsStorageScope,
digestHex: string,
): Promise<BrowserDataResult<boolean>>;
}
@@ -0,0 +1,238 @@
import type { Result } from "../../result.ts";
export type BrowserDataFailureCode =
| "ABORTED"
| "BLOCKED"
| "CONFLICT"
| "CORRUPT_DATA"
| "EXPIRED_RESOURCE"
| "INTEGRITY_FAILED"
| "INVALID_INPUT"
| "LIMIT_EXCEEDED"
| "MIGRATION_FAILED"
| "NOT_FOUND"
| "NOT_READABLE"
| "PERMISSION_DENIED"
| "POLICY_REJECTED"
| "QUOTA_EXCEEDED"
| "STALE_RESULT"
| "STORAGE_EVICTED"
| "UNAVAILABLE"
| "UNSUPPORTED";
export type BrowserDataOperation =
| "CACHE_ACTIVATE"
| "CACHE_DELETE"
| "CACHE_LOOKUP"
| "CACHE_STAGE"
| "DOWNLOAD"
| "FILE_INSPECT"
| "FILE_READ"
| "FILE_SELECT"
| "IMAGE_RESOLVE"
| "INDEXEDDB_MIGRATE"
| "INDEXEDDB_OPEN"
| "INDEXEDDB_READ"
| "INDEXEDDB_WRITE"
| "OBJECT_DELETE"
| "OBJECT_READ"
| "OBJECT_RECONCILE"
| "OBJECT_WRITE"
| "PRESIGNED_TRANSFER"
| "PREVIEW"
| "STORAGE_ESTIMATE"
| "STORAGE_PERSIST"
| "UPLOAD_ABORT"
| "UPLOAD_COMPLETE"
| "UPLOAD_PART"
| "UPLOAD_RECONCILE"
| "UPLOAD_SESSION";
export type BrowserDataRecovery =
| "NONE"
| "RETRY"
| "REOPEN"
| "RESELECT"
| "RELOAD_OTHER_CONTEXTS"
| "READ_ONLY"
| "ONLINE_ONLY"
| "REHYDRATE"
| "EXPORT_REQUIRED"
| "REISSUE_CAPABILITY"
| "RESUME"
| "RESTART"
| "RECONCILE";
/**
* Closed, telemetry-safe failure. Native exception messages, paths, record
* keys, URLs, file names and data values must not cross the adapter boundary.
*/
export type BrowserDataFailure = Readonly<{
code: BrowserDataFailureCode;
operation: BrowserDataOperation;
retryable: boolean;
recovery: BrowserDataRecovery;
}>;
export type BrowserDataResult<Value> = Result<Value, BrowserDataFailure>;
export type BrowserDataObservation = Readonly<{
operation: BrowserDataOperation;
outcome: "SUCCEEDED" | "FAILED" | "DEGRADED";
failureCode?: BrowserDataFailureCode;
durationBucket?: "LT100MS" | "100_TO_499MS" | "500_TO_1999MS" | "GTE2000MS";
byteBucket?: "ZERO" | "LT1MIB" | "1_TO_9MIB" | "10_TO_99MIB" | "GTE100MIB";
countBucket?: "ZERO" | "ONE" | "TWO_TO_TEN" | "ELEVEN_TO_HUNDRED" | "GT_HUNDRED";
}>;
export interface BrowserDataObserver {
record(observation: BrowserDataObservation): void;
}
export type TransferProgress = Readonly<{
phase:
| "VALIDATING"
| "PREPARING"
| "TRANSFERRING"
| "VERIFYING"
| "FINALIZING";
transferredBytes: number;
totalBytes: number | null;
}>;
/**
* Technology-neutral byte stream. Implementations must validate every emitted
* chunk, honor AbortSignal between chunks and close native/runtime failures
* into BrowserDataResult. A consumer must stop after the first failed chunk.
*/
export interface ByteSource {
readonly byteLength: number | null;
stream(
signal: AbortSignal,
): AsyncIterable<BrowserDataResult<Uint8Array>>;
}
export type PersistableDataClass =
| "PUBLIC"
| "INTERNAL"
| "PERSONAL"
| "CONFIDENTIAL";
export type BrowserDataAuthority =
| "SERVER"
| "LOCAL_FIRST"
| "RECONSTRUCTABLE";
export type BrowserAccountScope =
| "ORIGIN_SHARED"
| "OPAQUE_PARTITION";
export type BrowserLogoutAction =
| "KEEP_ORIGIN_SHARED"
| "PURGE_PARTITION"
| "EXPORT_THEN_PURGE";
export type BrowserAccountDeletionAction =
| "KEEP_ORIGIN_SHARED"
| "PURGE_PARTITION";
export type BrowserPressureAction =
| "EVICT_RECONSTRUCTABLE"
| "RETAIN";
export type BrowserStoragePolicy = Readonly<{
/** Governance owner/team identifier, never an account or user ID. */
owner: string;
namespace: string;
classification: PersistableDataClass;
authority: BrowserDataAuthority;
accountScope: BrowserAccountScope;
retention:
| Readonly<{ kind: "SESSION" }>
| Readonly<{ kind: "TTL"; maxAgeMs: number }>
| Readonly<{ kind: "UNTIL_SYNCED" }>
| Readonly<{ kind: "EXPLICIT_DELETE" }>;
softBudgetBytes: number;
hardBudgetBytes: number;
evictionPriority: "RECONSTRUCTABLE" | "SYNCED_COPY" | "USER_AUTHORED";
logoutAction: BrowserLogoutAction;
accountDeletionAction: BrowserAccountDeletionAction;
pressureAction: BrowserPressureAction;
unavailableFallback: "ONLINE_ONLY" | "READ_ONLY" | "EXPORT_REQUIRED";
}>;
export function isValidByteLength(value: number): boolean {
return Number.isSafeInteger(value) && value >= 0;
}
export function assertValidStoragePolicy(
policy: BrowserStoragePolicy,
): void {
const safeRegistryIdentifier =
/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
if (
!policy ||
typeof policy !== "object" ||
typeof policy.owner !== "string" ||
!safeRegistryIdentifier.test(policy.owner) ||
typeof policy.namespace !== "string" ||
!safeRegistryIdentifier.test(policy.namespace) ||
!["PUBLIC", "INTERNAL", "PERSONAL", "CONFIDENTIAL"].includes(
policy.classification,
) ||
!["SERVER", "LOCAL_FIRST", "RECONSTRUCTABLE"].includes(
policy.authority,
) ||
!["ORIGIN_SHARED", "OPAQUE_PARTITION"].includes(
policy.accountScope,
) ||
!policy.retention ||
typeof policy.retention !== "object" ||
!["SESSION", "TTL", "UNTIL_SYNCED", "EXPLICIT_DELETE"].includes(
policy.retention.kind,
) ||
!isValidByteLength(policy.softBudgetBytes) ||
!isValidByteLength(policy.hardBudgetBytes) ||
policy.hardBudgetBytes === 0 ||
policy.softBudgetBytes > policy.hardBudgetBytes ||
!["RECONSTRUCTABLE", "SYNCED_COPY", "USER_AUTHORED"].includes(
policy.evictionPriority,
) ||
![
"KEEP_ORIGIN_SHARED",
"PURGE_PARTITION",
"EXPORT_THEN_PURGE",
].includes(policy.logoutAction) ||
!["KEEP_ORIGIN_SHARED", "PURGE_PARTITION"].includes(
policy.accountDeletionAction,
) ||
!["EVICT_RECONSTRUCTABLE", "RETAIN"].includes(
policy.pressureAction,
) ||
!["ONLINE_ONLY", "READ_ONLY", "EXPORT_REQUIRED"].includes(
policy.unavailableFallback,
) ||
(policy.retention.kind === "TTL" &&
(!Number.isSafeInteger(policy.retention.maxAgeMs) ||
policy.retention.maxAgeMs < 1)) ||
(policy.accountScope === "ORIGIN_SHARED" &&
(policy.logoutAction !== "KEEP_ORIGIN_SHARED" ||
policy.accountDeletionAction !== "KEEP_ORIGIN_SHARED")) ||
(policy.accountScope === "OPAQUE_PARTITION" &&
(policy.logoutAction === "KEEP_ORIGIN_SHARED" ||
policy.accountDeletionAction !== "PURGE_PARTITION")) ||
(["PERSONAL", "CONFIDENTIAL"].includes(policy.classification) &&
policy.accountScope !== "OPAQUE_PARTITION") ||
(policy.accountScope === "OPAQUE_PARTITION" &&
(policy.retention.kind === "UNTIL_SYNCED" ||
(policy.authority === "LOCAL_FIRST" &&
policy.unavailableFallback === "EXPORT_REQUIRED")) &&
policy.logoutAction !== "EXPORT_THEN_PURGE") ||
(policy.pressureAction === "EVICT_RECONSTRUCTABLE" &&
(policy.authority !== "RECONSTRUCTABLE" ||
policy.evictionPriority !== "RECONSTRUCTABLE" ||
policy.retention.kind === "EXPLICIT_DELETE"))
) {
throw new TypeError("Browser storage policy is invalid.");
}
}
@@ -0,0 +1,22 @@
import type { BrowserDataResult } from "./shared.ts";
export type StorageEstimate = Readonly<{
usageBytes: number | null;
quotaBytes: number | null;
/**
* null means the engine did not expose a persistence-state query. It must
* not be collapsed into a false "not persisted" claim.
*/
persisted: boolean | null;
pressure: "UNKNOWN" | "NORMAL" | "PRESSURE" | "CRITICAL";
}>;
export interface StorageDurabilityPort {
inspect(signal?: AbortSignal): Promise<BrowserDataResult<StorageEstimate>>;
requestPersistence(input: {
reason: "PROTECT_UNSYNCED_USER_DATA";
userInitiated: true;
signal?: AbortSignal;
}): Promise<BrowserDataResult<"GRANTED" | "DENIED">>;
}
@@ -0,0 +1,36 @@
import type { Result } from "../../result.ts";
import type { AppFailure } from "../../../contracts/errors.ts";
export type BrowserRpcCallContext = Readonly<{
signal?: AbortSignal;
idempotencyKey?: string;
}>;
/**
* A feature gateway binds a semantic operation during composition and exposes
* only this typed port to its use case. Generated services, messages, endpoint
* IDs and transport metadata remain adapter-private.
*/
export type BrowserRpcUnaryPort<Input, Output> = Readonly<{
execute(
input: Input,
context?: BrowserRpcCallContext,
): Promise<Result<Output, AppFailure>>;
}>;
/**
* Server streams remain operation-bound outbound results. They are not a
* runtime-wide event bus and do not expose protocol frames or generated
* messages to application callers.
*/
export type BrowserRpcServerStreamPort<Input, Event> = Readonly<{
open(
input: Input,
context?: BrowserRpcCallContext,
): AsyncIterable<Result<Event, AppFailure>>;
}>;
export type BrowserRpcGenerationFence<Token = unknown> = Readonly<{
capture(): Token;
isCurrent(token: Token): boolean;
}>;
@@ -0,0 +1,6 @@
export type {
BrowserRpcCallContext,
BrowserRpcGenerationFence,
BrowserRpcServerStreamPort,
BrowserRpcUnaryPort,
} from "./browser-rpc.ts";
@@ -0,0 +1,32 @@
declare const authorizedDownloadCapabilityBrand: unique symbol;
declare const authorizedDownloadCapabilityReceiptBrand: unique symbol;
/**
* Telemetry-safe server-issued identifier. It is not a URL, credential,
* object-store key or authorization token.
*/
export type AuthorizedDownloadCapabilityReceipt = string & {
readonly [authorizedDownloadCapabilityReceiptBrand]:
"AuthorizedDownloadCapabilityReceipt";
};
/**
* Opaque GET-only download handle shared by the file-delivery and transfer
* ports. The adapter owns the corresponding URL/query/header binding in an
* identity vault, so structurally equal caller-created objects are rejected.
*/
export type AuthorizedDownloadCapability = Readonly<{
capabilityReceipt: AuthorizedDownloadCapabilityReceipt;
method: "GET";
binding: Readonly<{
kind: "DOWNLOAD";
resourceId: string;
}>;
mediaType: string;
byteLength: number;
maxBytes: number;
expectedSha256: string;
expiresAtEpochMs: number;
readonly [authorizedDownloadCapabilityBrand]:
"AuthorizedDownloadCapability";
}>;
@@ -0,0 +1,183 @@
import type { BrowserDataResult } from "../browser-file-storage/shared.ts";
declare const imageAssetReferenceBrand: unique symbol;
declare const imagePresetReferenceBrand: unique symbol;
export type ImageRasterMediaType =
| "image/avif"
| "image/jpeg"
| "image/png"
| "image/webp";
export type ImageOutputFormat = "avif" | "jpeg" | "png" | "webp";
export type ImageFit = "contain" | "cover" | "fill" | "inside" | "outside";
/**
* Identity capability backed by an adapter-owned WeakMap. A structurally equal
* object or a reference issued by another runtime must be rejected.
*/
export type ImageAssetReference = Readonly<{
readonly [imageAssetReferenceBrand]: "ImageAssetReference";
}>;
/**
* Composition-issued named preset reference. Presentation cannot submit
* width, height, DPR, quality, format, URL or query overrides.
*/
export type ImagePresetReference = Readonly<{
presetKey: string;
intention: string;
readonly [imagePresetReferenceBrand]: "ImagePresetReference";
}>;
export type PublicImmutableImageAsset = Readonly<{
kind: "ALLOWLISTED_PUBLIC";
originKey: string;
assetId: string;
revision: string;
mediaType: ImageRasterMediaType;
contentKind: "RASTER_STATIC";
intrinsicWidth: number;
intrinsicHeight: number;
}>;
/**
* Server-issued descriptor for private signed delivery. It contains no URL or
* request headers. The signature covers every immutable field and the exact
* set of registry-owned preset binding IDs.
*/
export type BackendIssuedImageAsset = Readonly<{
kind: "BACKEND_ISSUED_PRIVATE";
issuer: string;
originKey: string;
assetId: string;
revision: string;
mediaType: ImageRasterMediaType;
contentKind: "RASTER_STATIC";
intrinsicWidth: number;
intrinsicHeight: number;
capabilityId: string;
issuedAtEpochMs: number;
expiresAtEpochMs: number;
allowedPresetBindingIds: readonly string[];
signature: Readonly<{
algorithm: "ECDSA_P256_SHA256";
keyId: string;
capabilityBindingDigestHex: string;
valueBase64Url: string;
}>;
}>;
export type ImageCapabilityVerificationRequest = Readonly<{
algorithm: "ECDSA_P256_SHA256";
keyId: string;
canonicalPayload: Uint8Array;
signatureBase64Url: string;
}>;
export interface ImageCapabilityVerifier {
/** Exact membership check against the verifier's immutable key registry. */
acceptsKey(keyId: string): boolean;
verify(
request: ImageCapabilityVerificationRequest,
): Promise<boolean>;
}
export interface ImageAssetAcceptancePort {
acceptPublicImmutable(
descriptor: PublicImmutableImageAsset,
): BrowserDataResult<ImageAssetReference>;
acceptBackendIssued(
descriptor: BackendIssuedImageAsset,
options?: Readonly<{ signal?: AbortSignal }>,
): Promise<BrowserDataResult<ImageAssetReference>>;
}
export type ImageDeliveryClass =
| "PUBLIC_IMMUTABLE"
| "PRIVATE_SIGNED";
export type ImageProbeRequest = Readonly<{
absoluteUrl: string;
expectedMediaType: ImageRasterMediaType;
expectedWidth: number;
expectedHeight: number;
maxEncodedBytes: number;
maxDecodedPixels: number;
maxDecodedBytes: number;
delivery: ImageDeliveryClass;
minimumPublicMaxAgeSeconds: number;
referrerPolicy: "no-referrer" | "strict-origin-when-cross-origin";
signal: AbortSignal;
}>;
export type ImageProbeReceipt = Readonly<{
absoluteUrl: string;
mediaType: ImageRasterMediaType;
encodedBytes: number;
decodedWidth: number;
decodedHeight: number;
}>;
/**
* Optional browser-native seam. Implementations must bound the encoded body
* before buffering and close the decoded ImageBitmap after inspecting it.
*/
export interface ImageResourceProbePort {
probe(
request: ImageProbeRequest,
): Promise<BrowserDataResult<ImageProbeReceipt>>;
}
export type ImagePresentationSource = Readonly<{
type: ImageRasterMediaType;
srcSet: string;
}>;
export type ImagePresentationDescriptor = Readonly<{
src: string;
srcSet: string;
sources: readonly ImagePresentationSource[];
sizes: string;
width: number;
height: number;
fallbackMediaType: ImageRasterMediaType;
loading: "eager" | "lazy";
decoding: "async" | "sync";
fetchPriority: "high" | "low" | "auto";
referrerPolicy: "no-referrer" | "strict-origin-when-cross-origin";
crossOrigin: "anonymous";
delivery: Readonly<{
class: ImageDeliveryClass;
assetVersion: string;
browserCache: "PUBLIC_IMMUTABLE" | "NO_STORE";
sharedCache: "PUBLIC_IMMUTABLE" | "FORBIDDEN";
purge:
| "REVISION_ROLLOVER"
| "CAPABILITY_REVOCATION_OR_EXPIRY";
expiresAtEpochMs: number | null;
}>;
decodeBudget: Readonly<{
maximumCandidatePixels: number;
maximumDecodedBytes: number;
maximumEncodedBytes: number;
}>;
}>;
export interface ImageCdnPresentationPort {
resolve(request: Readonly<{
asset: ImageAssetReference;
preset: ImagePresetReference;
signal?: AbortSignal;
}>): Promise<BrowserDataResult<ImagePresentationDescriptor>>;
}
export type ImageCdnRuntime = Readonly<{
assets: ImageAssetAcceptancePort;
presentation: ImageCdnPresentationPort;
/**
* Terminal and idempotent. Aborts in-flight verification/probing, revokes
* every issued reference and makes later accept/resolve calls unavailable.
*/
close(): void;
}>;
@@ -0,0 +1,68 @@
export type {
AuthorizedDownloadCapability,
AuthorizedDownloadCapabilityReceipt,
} from "./authorized-download.ts";
export type {
BackendIssuedImageAsset,
ImageAssetAcceptancePort,
ImageAssetReference,
ImageCapabilityVerificationRequest,
ImageCapabilityVerifier,
ImageCdnPresentationPort,
ImageCdnRuntime,
ImageDeliveryClass,
ImageFit,
ImageOutputFormat,
ImagePresentationDescriptor,
ImagePresentationSource,
ImagePresetReference,
ImageProbeReceipt,
ImageProbeRequest,
ImageRasterMediaType,
ImageResourceProbePort,
PublicImmutableImageAsset,
} from "./image-cdn.ts";
export type {
PresignedDownloadByteSource,
PresignedDownloadCapability,
PresignedDownloadSourcePort,
PresignedTransferBinding,
PresignedTransferCapability,
PresignedTransferCapabilityProvider,
PresignedTransferCapabilityReceipt,
PresignedTransferMethod,
PresignedTransferReplayGuard,
PresignedUploadPartCapability,
PresignedUploadPartCapabilityProvider,
PresignedUploadPartOutcome,
PresignedUploadPartPort,
} from "./presigned-transfer.ts";
export type {
ActiveUploadStatus,
QuarantinedUpload,
ResumableUploadCheckpoint,
ResumableUploadCheckpointAdmin,
ResumableUploadCheckpointStore,
ResumableUploadControlPlane,
ResumableUploadPort,
ResumableUploadRequest,
ResumableUploadSource,
UploadAbortOutcome,
UploadFileFingerprint,
UploadPartCapability,
UploadPartDescriptor,
UploadPartExecutor,
UploadPartReceipt,
UploadProviderFailure,
UploadProviderResult,
UploadRangeReader,
UploadSession,
UploadSessionStatus,
} from "./resumable-upload.ts";
export {
RESUMABLE_UPLOAD_PROTOCOL,
type ResumableUploadProtocol,
} from "./resumable-upload.ts";
@@ -0,0 +1,151 @@
import type { FileByteSource } from "../browser-file-storage/file.ts";
import type { BrowserDataResult } from "../browser-file-storage/shared.ts";
import type {
AuthorizedDownloadCapability,
AuthorizedDownloadCapabilityReceipt,
} from "./authorized-download.ts";
import type { ResumableUploadProtocol } from "./resumable-upload.ts";
declare const presignedTransferCapabilityBrand: unique symbol;
/**
* Server-issued, telemetry-safe identifier. It is not a URL, credential,
* object-store key or authorization token.
*/
export type PresignedTransferCapabilityReceipt =
AuthorizedDownloadCapabilityReceipt;
export type PresignedTransferMethod = "GET" | "PUT";
export type PresignedTransferBinding =
| Readonly<{
kind: "DOWNLOAD";
resourceId: string;
}>
| Readonly<{
kind: "UPLOAD_PART";
protocol: ResumableUploadProtocol;
sessionId: string;
requestBindingSha256: string;
/**
* SHA-256 over the canonical session, request and whole-file fingerprint
* binding. The raw session fields remain owned by the upload control
* plane; they must never be smuggled into resourceId.
*/
uploadBindingSha256: string;
partNumber: number;
offset: number;
idempotencyKey: string;
}>;
/**
* Opaque capability handle crossing the application boundary. The adapter owns
* the corresponding URL, query values and request headers in an in-memory
* identity vault. Implementations must reject structurally equal or fabricated
* handles, even if every visible field matches.
*/
export type PresignedDownloadCapability = AuthorizedDownloadCapability;
export type PresignedUploadPartCapability = Readonly<{
capabilityReceipt: PresignedTransferCapabilityReceipt;
method: "PUT";
binding: Extract<
PresignedTransferBinding,
Readonly<{ kind: "UPLOAD_PART" }>
>;
mediaType: string;
byteLength: number;
maxBytes: number;
expectedSha256: string;
expiresAtEpochMs: number;
readonly [presignedTransferCapabilityBrand]:
"PresignedTransferCapability";
}>;
export type PresignedTransferCapability =
| PresignedDownloadCapability
| PresignedUploadPartCapability;
export interface PresignedTransferCapabilityProvider {
/**
* Calls a composition-owned backend/BFF capability endpoint. Callers choose
* only an opaque resource ID; they cannot supply a transfer URL or headers.
*/
issueDownload(input: Readonly<{
resourceId: string;
signal: AbortSignal;
}>): Promise<BrowserDataResult<PresignedDownloadCapability>>;
}
export interface PresignedUploadPartCapabilityProvider {
issueUploadPart(input: Readonly<{
sessionId: string;
requestBindingSha256: string;
uploadBindingSha256: string;
partNumber: number;
offset: number;
byteLength: number;
checksumSha256: string;
mediaType: string;
idempotencyKey: string;
signal: AbortSignal;
}>): Promise<BrowserDataResult<PresignedUploadPartCapability>>;
}
/**
* Atomic local replay seam. The server/object-store capability must also
* enforce single use or equivalent idempotency because a browser guard is not
* an authorization boundary.
*/
export interface PresignedTransferReplayGuard {
claim(
capability: PresignedTransferCapability,
): BrowserDataResult<true>;
}
/**
* Successful exhaustion proves length and SHA-256 before the terminal success
* of the closed-Result stream. Consumers must not commit a destination until
* the iterable finishes without a failure result.
*/
export type PresignedDownloadByteSource = FileByteSource &
Readonly<{
byteLength: number;
capability: PresignedDownloadCapability;
integrity: "VERIFIED_ON_SUCCESSFUL_EXHAUSTION";
}>;
export interface PresignedDownloadSourcePort {
open(input: Readonly<{
resourceId: string;
capability: PresignedDownloadCapability;
signal: AbortSignal;
}>): Promise<BrowserDataResult<PresignedDownloadByteSource>>;
}
export type PresignedUploadPartOutcome = Readonly<{
bytesWritten: number;
checksumSha256: string;
/**
* Non-authorizing object-store acknowledgement (for example a normalized
* ETag). It is safe to persist only as part of the exact completed-part
* binding and must never be reused as a transfer capability.
*/
receiptToken: string;
}>;
export interface PresignedUploadPartPort {
put(input: Readonly<{
capability: PresignedUploadPartCapability;
sessionId: string;
requestBindingSha256: string;
uploadBindingSha256: string;
partNumber: number;
offset: number;
byteLength: number;
checksumSha256: string;
idempotencyKey: string;
bytes: Uint8Array;
signal: AbortSignal;
}>): Promise<BrowserDataResult<PresignedUploadPartOutcome>>;
}
@@ -0,0 +1,279 @@
import type { Result } from "../../result.ts";
import type { FileByteSource } from "../browser-file-storage/file.ts";
import type {
BrowserDataFailure,
BrowserDataResult,
TransferProgress,
} from "../browser-file-storage/shared.ts";
export const RESUMABLE_UPLOAD_PROTOCOL =
"PRESIGNED_MULTIPART_V1" as const;
export type ResumableUploadProtocol =
typeof RESUMABLE_UPLOAD_PROTOCOL;
/**
* Multipart uploads use a bounded part manifest instead of a whole-file
* ArrayBuffer. The digest is SHA-256 over the canonical ordered part metadata
* and SHA-256 part digests.
*/
export type UploadFileFingerprint = Readonly<{
algorithm: "SHA-256-PARTS-V1";
digestHex: string;
byteLength: number;
partSizeBytes: number;
partCount: number;
}>;
export type UploadPartDescriptor = Readonly<{
partNumber: number;
offset: number;
byteLength: number;
checksumSha256: string;
}>;
/**
* A non-authorizing server acknowledgement. It must be opaque, contain no PII,
* URL or credential, and be accepted only with the exact descriptor binding.
*/
export type UploadPartReceipt = UploadPartDescriptor &
Readonly<{
receiptToken: string;
}>;
export interface UploadRangeReader {
readonly byteLength: number;
readRange(input: Readonly<{
offset: number;
length: number;
signal: AbortSignal;
}>): Promise<BrowserDataResult<Uint8Array>>;
}
/**
* FILE_BYTE_SOURCE supports existing FileByteSource implementations. It must
* be replayable for the fingerprint pass and transfer pass. RANGE_READER is
* preferred for concurrent uploads and OPFS/file-vault range adapters.
*/
export type ResumableUploadSource =
| Readonly<{
kind: "FILE_BYTE_SOURCE";
bytes: FileByteSource;
}>
| Readonly<{
kind: "RANGE_READER";
reader: UploadRangeReader;
}>;
export type ResumableUploadRequest = Readonly<{
/** Opaque, caller-stable operation key. It must not contain a file name. */
uploadKey: string;
/** Registry-approved backend purpose identifier, not user-provided text. */
purpose: string;
mediaType: string;
source: ResumableUploadSource;
signal: AbortSignal;
onProgress?: (progress: TransferProgress) => void;
}>;
export type QuarantinedUpload = Readonly<{
state: "QUARANTINED";
resourceId: string;
byteLength: number;
replayed: boolean;
}>;
export type UploadAbortOutcome = Readonly<{
state: "ABORTED" | "ORPHANED" | "ALREADY_COMPLETED" | "NOT_FOUND";
}>;
export interface ResumableUploadPort {
upload(
request: ResumableUploadRequest,
): Promise<BrowserDataResult<QuarantinedUpload>>;
abort(request: Readonly<{
uploadKey: string;
signal: AbortSignal;
}>): Promise<BrowserDataResult<UploadAbortOutcome>>;
}
/**
* Retry-After is transport metadata used only by the runtime. It is bounded
* before sleeping and is removed from the application-facing failure.
*/
export type UploadProviderFailure = BrowserDataFailure &
Readonly<{
retryAfterMs?: number;
}>;
export type UploadProviderResult<Value> = Result<
Value,
UploadProviderFailure
>;
export type UploadSession = Readonly<{
protocol: ResumableUploadProtocol;
sessionId: string;
requestBindingSha256: string;
fingerprint: UploadFileFingerprint;
partSizeBytes: number;
partCount: number;
maxConcurrency: number;
expiresAtEpochMs: number;
}>;
export type ActiveUploadStatus = Readonly<{
state: "ACTIVE";
session: UploadSession;
acceptedParts: readonly UploadPartReceipt[];
}>;
export type UploadSessionStatus =
| ActiveUploadStatus
| Readonly<{
state: "QUARANTINED";
session: UploadSession;
resourceId: string;
}>
| Readonly<{
state: "ABORTED" | "EXPIRED" | "NOT_FOUND";
protocol: ResumableUploadProtocol;
sessionId: string;
requestBindingSha256: string;
}>;
/**
* The capability value is intentionally generic. The presigned-transfer
* adapter owns its URL/method/header contract; this port neither duplicates
* that type nor permits it to enter a durable checkpoint.
*/
export type UploadPartCapability<Capability> = Readonly<{
capability: Capability;
uploadBindingSha256: string;
expiresAtEpochMs: number;
}>;
export interface ResumableUploadControlPlane<Capability> {
createSession(input: Readonly<{
protocol: ResumableUploadProtocol;
uploadKey: string;
purpose: string;
mediaType: string;
requestBindingSha256: string;
fingerprint: UploadFileFingerprint;
requestedPartSizeBytes: number;
requestedMaxConcurrency: number;
idempotencyKey: string;
signal: AbortSignal;
}>): Promise<UploadProviderResult<UploadSession>>;
getStatus(input: Readonly<{
protocol: ResumableUploadProtocol;
sessionId: string;
requestBindingSha256: string;
fingerprint: UploadFileFingerprint;
signal: AbortSignal;
}>): Promise<UploadProviderResult<UploadSessionStatus>>;
issuePartCapability(input: Readonly<{
protocol: ResumableUploadProtocol;
sessionId: string;
requestBindingSha256: string;
uploadBindingSha256: string;
fingerprint: UploadFileFingerprint;
mediaType: string;
part: UploadPartDescriptor;
idempotencyKey: string;
signal: AbortSignal;
}>): Promise<UploadProviderResult<UploadPartCapability<Capability>>>;
complete(input: Readonly<{
protocol: ResumableUploadProtocol;
sessionId: string;
requestBindingSha256: string;
fingerprint: UploadFileFingerprint;
orderedParts: readonly UploadPartReceipt[];
idempotencyKey: string;
signal: AbortSignal;
}>): Promise<UploadProviderResult<Readonly<{
state: "QUARANTINED";
protocol: ResumableUploadProtocol;
sessionId: string;
requestBindingSha256: string;
fingerprint: UploadFileFingerprint;
resourceId: string;
}>>>;
abort(input: Readonly<{
protocol: ResumableUploadProtocol;
sessionId: string;
requestBindingSha256: string;
idempotencyKey: string;
signal: AbortSignal;
}>): Promise<UploadProviderResult<Readonly<{
state: "ABORTED" | "NOT_FOUND" | "EXPIRED" | "ALREADY_COMPLETED";
}>>>;
}
export interface UploadPartExecutor<Capability> {
uploadPart(input: Readonly<{
protocol: ResumableUploadProtocol;
capability: Capability;
sessionId: string;
requestBindingSha256: string;
uploadBindingSha256: string;
fingerprint: UploadFileFingerprint;
mediaType: string;
part: UploadPartDescriptor;
bytes: Uint8Array;
idempotencyKey: string;
signal: AbortSignal;
}>): Promise<UploadProviderResult<UploadPartReceipt>>;
}
/**
* Durable, non-secret recovery state. Implementations must reject any unknown
* property so a signed URL, authorization header or user metadata cannot be
* smuggled into persistence.
*/
export type ResumableUploadCheckpoint = Readonly<{
schemaVersion: 1;
protocol: ResumableUploadProtocol;
revision: number;
state: "ACTIVE" | "ABORT_PENDING";
uploadKey: string;
requestBindingSha256: string;
fingerprint: UploadFileFingerprint;
sessionId: string;
sessionExpiresAtEpochMs: number;
sessionMaxConcurrency: number;
acceptedParts: readonly UploadPartReceipt[];
updatedAtEpochMs: number;
}>;
export interface ResumableUploadCheckpointStore {
read(
uploadKey: string,
signal?: AbortSignal,
): Promise<BrowserDataResult<ResumableUploadCheckpoint | null>>;
compareAndSwap(input: Readonly<{
expectedRevision: number | null;
checkpoint: ResumableUploadCheckpoint;
signal?: AbortSignal;
}>): Promise<BrowserDataResult<ResumableUploadCheckpoint>>;
remove(input: Readonly<{
uploadKey: string;
expectedRevision: number;
signal?: AbortSignal;
}>): Promise<BrowserDataResult<void>>;
close(): void;
}
export interface ResumableUploadCheckpointAdmin {
/**
* Account/logout lifecycle operation for this already-bound opaque partition.
* The adapter closes its connection before deletion and bounds blocked waits.
*/
deletePartition(
signal?: AbortSignal,
): Promise<BrowserDataResult<Readonly<{ state: "DELETED" }>>>;
}
+4
View File
@@ -0,0 +1,4 @@
export type ClockPort = Readonly<{
now(): number;
sleep(milliseconds: number, signal?: AbortSignal): Promise<void>;
}>;
@@ -0,0 +1,5 @@
import type { DiagnosticRecordInput } from "../../contracts/diagnostics.ts";
export type DiagnosticsPort = Readonly<{
record(input: DiagnosticRecordInput): void;
}>;
@@ -0,0 +1,83 @@
import type { SessionState } from "../auth-session-port.ts";
import type { RuntimeCapabilitySnapshot } from "../runtime-capabilities-port.ts";
import type { StoragePort } from "../storage-port.ts";
export type { SessionState } from "../auth-session-port.ts";
export type { RuntimeCapabilitySnapshot };
/**
* Features add their driving API through module augmentation. The application
* owns the registry contract without importing any concrete feature.
*/
export interface ApplicationFeatureInputs {}
export type ApplicationFeatureId = Extract<
keyof ApplicationFeatureInputs,
string
>;
export type ColorSchemePreference = "system" | "light" | "dark";
export type RenderFailureReport = Readonly<{
routeId: string;
buildId: string;
boundaryName: "route" | "feature";
}>;
export type RouteChangedReport = Readonly<{
routeId: string;
buildId: string;
}>;
export type ReleaseSummary = Readonly<{
buildId: string;
releaseId: string;
configSchemaVersion: string;
/** Legacy V1 scalar; absent once the release manifest is V2. */
apiContractVersion?: string;
/** §5.2 contract set identity for a V2 release manifest. */
contractSetDigest?: string;
}>;
export type ApplicationApi = Readonly<{
session: Readonly<{
getSnapshot(): SessionState;
subscribe(listener: () => void): () => void;
beginSignIn(returnTo?: string): Promise<void>;
signOut(): Promise<void>;
recover(): Promise<"restored" | "no-session">;
}>;
preferences: Readonly<{
getColorScheme(): ColorSchemePreference;
setColorScheme(
preference: ColorSchemePreference,
): ReturnType<StoragePort["write"]>;
}>;
diagnostics: Readonly<{
reportRenderFailure(report: RenderFailureReport): void;
reportRouteChanged(report: RouteChangedReport): void;
}>;
runtime: Readonly<{
getReleaseSummary(): Promise<ReleaseSummary>;
/**
* §3.5. The static selection reduced by the runtime overrides. Presentation
* reads capability state here instead of importing the composition root.
*/
getCapabilitySnapshot(): RuntimeCapabilitySnapshot;
}>;
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 }>
>;
}>;
features: Readonly<{
has(featureId: string): featureId is ApplicationFeatureId;
get<FeatureId extends ApplicationFeatureId>(
featureId: FeatureId,
): ApplicationFeatureInputs[FeatureId];
}>;
}>;
+9
View File
@@ -0,0 +1,9 @@
export type {
ApplicationApi,
ApplicationFeatureId,
ApplicationFeatureInputs,
ColorSchemePreference,
ReleaseSummary,
RenderFailureReport,
SessionState,
} from "./application-api.ts";
@@ -0,0 +1,11 @@
import type { MutationIntent } from "../../contracts/mutation-intent.ts";
export type MutationIntentFactoryInput = Readonly<{
operationId: string;
canonicalInputIdentity: string;
requiresIdempotencyKey: boolean;
}>;
export type MutationIntentFactory = Readonly<{
create(input: MutationIntentFactoryInput): MutationIntent;
}>;
@@ -0,0 +1,23 @@
import type { AuthSessionPort } from "../auth-session-port.ts";
import type { ReleaseInfoPort } from "../release-info-port.ts";
import type { RuntimeCapabilitiesPort } from "../runtime-capabilities-port.ts";
import type { StoragePort } from "../storage-port.ts";
import type { TelemetryPort } from "../telemetry-port.ts";
import type { DiagnosticsPort } from "../diagnostics-port.ts";
/**
* Capabilities required by application use cases. Implementations live in
* outbound adapters and are selected only by bootstrap.
*/
export type ApplicationOutputPorts = Readonly<{
session: Pick<
AuthSessionPort,
"getState" | "subscribe" | "beginSignIn" | "signOut" | "recover"
>;
preferences: StoragePort;
diagnostics: DiagnosticsPort;
telemetry: TelemetryPort;
releaseInfo: ReleaseInfoPort;
runtimeCapabilities: RuntimeCapabilitiesPort;
navigation: Readonly<{ reload(): void }>;
}>;
+19
View File
@@ -0,0 +1,19 @@
export type { ApplicationOutputPorts } from "./application-output-ports.ts";
export type {
AuthSessionPort,
CredentialAttacher,
SessionGateway,
} from "../auth-session-port.ts";
export type { ClockPort } from "../clock-port.ts";
export type { QueryCachePort } from "../query-cache-port.ts";
export type { ReleaseInfoPort } from "../release-info-port.ts";
export type { StoragePort } from "../storage-port.ts";
export type { TelemetryPort } from "../telemetry-port.ts";
export type { DiagnosticsPort } from "../diagnostics-port.ts";
export type { WebPushControlPort } from "./web-push-control.ts";
export type {
BrowserRpcCallContext,
BrowserRpcGenerationFence,
BrowserRpcServerStreamPort,
BrowserRpcUnaryPort,
} from "../browser-rpc/index.ts";
@@ -0,0 +1,31 @@
import type {
PushAuthoritySnapshot,
WebPushReadiness,
WebPushResult,
} from "../../../contracts/web-push.ts";
export interface WebPushControlPort {
inspect(input: Readonly<{
authority: PushAuthoritySnapshot;
signal?: AbortSignal;
}>): Promise<WebPushResult<WebPushReadiness>>;
enable(input: Readonly<{
authority: PushAuthoritySnapshot;
signal: AbortSignal;
}>): Promise<WebPushResult<WebPushReadiness>>;
reconcile(input: Readonly<{
authority: PushAuthoritySnapshot;
signal?: AbortSignal;
}>): Promise<WebPushResult<WebPushReadiness>>;
revoke(input: Readonly<{
previousAuthority: PushAuthoritySnapshot;
nextAuthority: PushAuthoritySnapshot;
signal?: AbortSignal;
}>): Promise<WebPushResult<WebPushReadiness>>;
dispose(): void;
}
+15
View File
@@ -0,0 +1,15 @@
import type { ApiFailure } from "../../contracts/errors.ts";
export type QueryCacheReadResult =
| Readonly<{ ok: true; value: unknown }>
| Readonly<{ ok: false; error: ApiFailure }>;
export type QueryCacheWriteResult =
| Readonly<{ ok: true }>
| Readonly<{ ok: false; error: ApiFailure }>;
export type QueryCachePort = Readonly<{
read(key: readonly unknown[]): QueryCacheReadResult;
write(key: readonly unknown[], value: unknown): QueryCacheWriteResult;
invalidate(namespace: readonly unknown[]): Promise<QueryCacheWriteResult>;
}>;
@@ -0,0 +1,202 @@
import type {
EventTypeId,
ExternalEventEffectProfileId,
StreamRegistrationId,
} from "../../../contracts/realtime-streams.ts";
import type {
RealtimeResumeState,
SnapshotCheckpoint,
} from "../../../contracts/realtime-events.ts";
import type {
RealtimeFailureKind,
RealtimeResult,
} from "./shared.ts";
declare const realtimeRecoveryCheckpointBrand: unique symbol;
/**
* An in-memory, one-generation recovery lease. Callers must retain the exact
* object returned by the coordinator; reconstructing an equal checkpoint does
* not authorize a transport-barrier commit.
*/
export type RealtimeRecoveryCheckpoint = RealtimeResumeState &
Readonly<{ [realtimeRecoveryCheckpointBrand]: true }>;
export type RealtimeScopeSnapshot = Readonly<{
generation: number;
/**
* Session/BFF-issued opaque binding. This is not a cache fingerprint or an
* authorization credential and must never be projected into diagnostics.
*/
scopeBinding: string;
isCurrent(): boolean;
}>;
export type ExternalRealtimeEventContext = Readonly<{
streamId: StreamRegistrationId;
eventType: EventTypeId;
occurredAt: string;
scopeGeneration: number;
/**
* Per-callback commit authority. The effect owner must check this immediately
* before its final local commit. It becomes permanently false when the
* callback settles, even if the captured scope itself is still current.
*/
isCurrent(): boolean;
}>;
export type RealtimeEventEffectAuthority = Readonly<{
/**
* Resolves a registry-owned profile to a feature-owned application input and
* commits its local effect. Success means the complete local effect has
* committed; only then may the coordinator advance its checkpoint.
*/
apply(
effectProfileId: ExternalEventEffectProfileId,
event: unknown,
context: ExternalRealtimeEventContext,
signal: AbortSignal,
): Promise<RealtimeResult<void>>;
}>;
export type RealtimeRecoveryReason =
| "INITIALIZE"
| "STREAM_EPOCH_CHANGED"
| "SEQUENCE_GAP"
| "CURSOR_EXPIRED"
| "QUEUE_OVERFLOW"
| "DEDUPE_OVERFLOW"
| "EVENT_CONFLICT"
| "MAPPING_CONTRACT_VIOLATION"
| "APPLY_FAILED"
| "SCOPE_PROTOCOL_VIOLATION";
export type RealtimeRecoveryRequest = Readonly<{
streamId: StreamRegistrationId;
reason: RealtimeRecoveryReason;
scopeGeneration: number;
signal: AbortSignal;
/**
* Per-recovery commit authority. Snapshot/rebuild projection owners must
* check this immediately before commit. It becomes permanently false when
* `recover` settles.
*/
isCurrent(): boolean;
}>;
export type RealtimeRecoveryCommit =
| Readonly<{
kind: "SNAPSHOT_RESET";
checkpoint: SnapshotCheckpoint;
}>
| Readonly<{
kind: "SESSION_REBUILD";
streamEpoch: string;
lastAppliedSequence: string;
}>;
export type RealtimeRecoveryAuthority = Readonly<{
/**
* Success is an authoritative commit: all required projections have already
* been applied for the returned checkpoint in the captured scope.
*/
recover(
request: RealtimeRecoveryRequest,
): Promise<RealtimeResult<RealtimeRecoveryCommit>>;
}>;
export type RealtimeEventAuthority = Readonly<{
effects: RealtimeEventEffectAuthority;
recovery: RealtimeRecoveryAuthority;
}>;
export type RealtimeAcceptDropReason =
| "CLOSED"
| "DUPLICATE_EVENT"
| "RECOVERY_IN_PROGRESS"
| "SCOPE_FENCED"
| "STALE_EVENT";
export type RealtimeAcceptDisposition =
| Readonly<{
outcome: "APPLIED";
resumeState: RealtimeResumeState;
}>
| Readonly<{
outcome: "DROPPED";
reason: RealtimeAcceptDropReason;
}>
| Readonly<{
outcome: "RECOVERED";
reason: RealtimeRecoveryReason;
resumeState: RealtimeRecoveryCheckpoint;
}>
| Readonly<{
outcome: "RECOVERY_BARRIER_REQUIRED";
resumeState: RealtimeRecoveryCheckpoint;
}>;
export type RealtimeTransportEventOutcome =
| Readonly<{ kind: "CONTINUE" }>
| Readonly<{
kind: "RECOVERY_COMMITTED";
streamId: StreamRegistrationId;
checkpoint: RealtimeRecoveryCheckpoint;
}>;
export const REALTIME_TRANSPORT_CONTINUE: RealtimeTransportEventOutcome =
Object.freeze({ kind: "CONTINUE" });
export function realtimeTransportRecoveryCommitted(
streamId: StreamRegistrationId,
checkpoint: RealtimeRecoveryCheckpoint,
): Extract<
RealtimeTransportEventOutcome,
{ kind: "RECOVERY_COMMITTED" }
> {
return Object.freeze({
kind: "RECOVERY_COMMITTED",
streamId,
checkpoint,
});
}
export type RealtimeStreamFreshness =
| "UNKNOWN"
| "CURRENT"
| "STALE"
| "RESYNCING";
export type RealtimeStreamInspection = Readonly<{
freshness: RealtimeStreamFreshness;
queuedEvents: number;
queuedBytes: number;
dedupeEntries: number;
closed: boolean;
hasResumeState: boolean;
awaitingTransportBarrier: boolean;
}>;
export type RealtimeObservationOutcome =
| "ACCEPTED"
| "APPLIED"
| "DROPPED"
| "FAILED"
| "RECOVERED";
/**
* Safe low-cardinality observation. It intentionally has no event identifier,
* sequence, payload, cursor, epoch, scope binding or native error.
*/
export type RealtimeObservation = Readonly<{
operation: "RECEIVE" | "APPLY" | "RECOVER" | "CLOSE";
outcome: RealtimeObservationOutcome;
streamId?: StreamRegistrationId;
eventType?: EventTypeId;
reason?: RealtimeFailureKind | RealtimeRecoveryReason;
queueSizeBucket?: "0" | "1-8" | "9-64" | "65-256" | "OVERFLOW";
}>;
export type RealtimeEventObservationSink = (
observation: RealtimeObservation,
) => void;
+31
View File
@@ -0,0 +1,31 @@
export type {
ExternalRealtimeEventContext,
RealtimeAcceptDisposition,
RealtimeAcceptDropReason,
RealtimeEventAuthority,
RealtimeEventEffectAuthority,
RealtimeEventObservationSink,
RealtimeObservation,
RealtimeObservationOutcome,
RealtimeRecoveryCheckpoint,
RealtimeRecoveryAuthority,
RealtimeRecoveryCommit,
RealtimeRecoveryReason,
RealtimeRecoveryRequest,
RealtimeScopeSnapshot,
RealtimeStreamFreshness,
RealtimeStreamInspection,
RealtimeTransportEventOutcome,
} from "./event-authority.ts";
export {
REALTIME_TRANSPORT_CONTINUE,
realtimeTransportRecoveryCommitted,
} from "./event-authority.ts";
export {
REALTIME_FAILURE_KINDS,
REALTIME_OPERATIONS,
type RealtimeFailure,
type RealtimeFailureKind,
type RealtimeOperation,
type RealtimeResult,
} from "./shared.ts";
+67
View File
@@ -0,0 +1,67 @@
export const REALTIME_OPERATIONS = Object.freeze([
"REGISTRY",
"DECODE",
"CONNECT",
"SUBSCRIBE",
"RECEIVE",
"SEND",
"APPLY",
"RECOVER",
"POLL",
"PUSH_REGISTER",
"PUSH_REVOKE",
"CLOSE",
] as const);
export type RealtimeOperation = (typeof REALTIME_OPERATIONS)[number];
export const REALTIME_FAILURE_KINDS = Object.freeze([
"ABORTED",
"UNSUPPORTED",
"OFFLINE",
"CONNECT_TIMEOUT",
"IDLE_TIMEOUT",
"AUTH_REQUIRED",
"FORBIDDEN",
"RATE_LIMITED",
"PROVIDER_UNAVAILABLE",
"PROTOCOL_MISMATCH",
"MALFORMED_EVENT",
"MAPPING_CONTRACT_VIOLATION",
"EVENT_CONFLICT",
"EVENT_TOO_LARGE",
"DUPLICATE_EVENT",
"STALE_EVENT",
"SEQUENCE_GAP",
"CURSOR_EXPIRED",
"QUEUE_OVERFLOW",
"APPLY_FAILED",
"POLL_BUDGET_EXHAUSTED",
"PUSH_PERMISSION_DENIED",
"PUSH_SUBSCRIPTION_STALE",
"NOTIFICATION_REJECTED",
"SCOPE_FENCED",
"SCOPE_PROTOCOL_VIOLATION",
"CLOSED",
] as const);
export type RealtimeFailureKind =
(typeof REALTIME_FAILURE_KINDS)[number];
/**
* Failure projected across realtime adapter boundaries.
*
* Native exceptions, frames, payloads, cursors, endpoints and scope bindings
* are deliberately absent. Adapters may observe those values locally while
* classifying a failure, but cannot expose them through this result.
*/
export type RealtimeFailure = Readonly<{
kind: RealtimeFailureKind;
operation: RealtimeOperation;
retryable: boolean;
}>;
export type RealtimeResult<Value> =
| Readonly<{ ok: true; value: Value }>
| Readonly<{ ok: false; error: RealtimeFailure }>;
@@ -0,0 +1,36 @@
export type ReleaseInfo = Readonly<{
schemaVersion?: number;
appVersion?: string;
buildId: string;
commitSha?: string;
configSchemaVersion: string;
/**
* §5.1. Legacy scalar, present only while a V1 release manifest is still
* accepted. A V2 manifest expresses contract identity through
* {@link ReleaseInfo.contractSetDigest}.
*/
apiContractVersion?: string;
contractSetDigest?: string;
assetManifestHash: string;
releaseId: string;
builtAt?: string;
routeChunks: Readonly<Record<string, string>>;
}>;
export type ActiveReleaseInfo = Readonly<
Pick<
ReleaseInfo,
| "buildId"
| "configSchemaVersion"
| "apiContractVersion"
| "contractSetDigest"
| "assetManifestHash"
| "releaseId"
| "routeChunks"
>
>;
export type ReleaseInfoPort = Readonly<{
getCurrent(): Promise<ReleaseInfo>;
refresh(): Promise<ActiveReleaseInfo>;
}>;
@@ -0,0 +1,12 @@
import type { RuntimeCapabilitySnapshot } from "../../contracts/runtime-capabilities.ts";
export type { RuntimeCapabilitySnapshot };
/**
* §3.5. The application reads capability state; it never resolves it. Only the
* composition root knows the runtime overrides, so the snapshot arrives here
* already reduced to counts and cannot be used to reach a runtime object.
*/
export type RuntimeCapabilitiesPort = Readonly<{
getSnapshot(): RuntimeCapabilitySnapshot;
}>;
+15
View File
@@ -0,0 +1,15 @@
import type { ApiFailure } from "../../contracts/errors.ts";
export type StorageReadResult =
| Readonly<{ ok: true; value: unknown }>
| Readonly<{ ok: false; error: ApiFailure }>;
export type StorageMutationResult =
| Readonly<{ ok: true }>
| Readonly<{ ok: false; error: ApiFailure; fallback?: string }>;
export type StoragePort = Readonly<{
read(logicalName: string): StorageReadResult;
write(logicalName: string, value: unknown): StorageMutationResult;
remove(logicalName: string): StorageMutationResult;
}>;
+10
View File
@@ -0,0 +1,10 @@
import type { TelemetryEventName } from "../../contracts/telemetry.ts";
export type { TelemetryEventName };
export type TelemetryPort = Readonly<{
emit(
eventName: TelemetryEventName,
attributes: Record<string, unknown>,
): void;
}>;
+10
View File
@@ -0,0 +1,10 @@
import type { AppFailure } from "../contracts/errors.ts";
/**
* The single success/failure carrier used across application input boundaries.
* Adapters map technology-specific errors to an application failure before
* constructing this value.
*/
export type Result<Value, Failure = AppFailure> =
| Readonly<{ ok: true; value: Value }>
| Readonly<{ ok: false; error: Failure }>;
@@ -0,0 +1,51 @@
import type { StoragePort } from "../ports/storage-port.ts";
const RECOVERABLE_KINDS = new Set(["CHUNK_LOAD_FAILURE", "DEPLOY_MISMATCH"]);
export type ChunkRecoveryDecision =
| Readonly<{ action: "reload-once"; releasePair: string }>
| Readonly<{ action: "support"; reason: string }>;
export type ChunkRecoveryInput = Readonly<{
failureKind: string;
manifestLoaded: boolean;
currentBuildId: string;
currentReleaseId: string;
activeBuildId: string;
activeReleaseId: string;
storage: StoragePort;
}>;
export function decideChunkRecovery(
input: ChunkRecoveryInput,
): ChunkRecoveryDecision {
if (!RECOVERABLE_KINDS.has(input.failureKind)) {
return { action: "support", reason: "not-recoverable" };
}
if (!input.manifestLoaded) {
return { action: "support", reason: "manifest-unavailable" };
}
if (
input.activeBuildId === input.currentBuildId &&
input.activeReleaseId === input.currentReleaseId
) {
return { action: "support", reason: "same-release" };
}
const releasePair =
`${input.currentBuildId}/${input.currentReleaseId}` +
`->${input.activeBuildId}/${input.activeReleaseId}`;
const guard = input.storage.read("CHUNK_RELOAD_GUARD");
if (!guard.ok) {
return { action: "support", reason: "guard-read-failed" };
}
if (guard.value === releasePair) {
return { action: "support", reason: "reload-already-attempted" };
}
const recorded = input.storage.write("CHUNK_RELOAD_GUARD", releasePair);
if (!recorded.ok) {
return { action: "support", reason: "guard-write-failed" };
}
return { action: "reload-once", releasePair };
}
+187
View File
@@ -0,0 +1,187 @@
import type { AppFailure } from "../../contracts/errors.ts";
export const ASYNC_BASE_STATES = Object.freeze([
"initial-loading",
"success",
"empty",
"terminal-error",
] as const);
export const ASYNC_OVERLAYS = Object.freeze([
"refreshing",
"stale-degraded",
"mutation-pending",
"mutation-effect-unknown",
"mutation-conflict",
] as const);
export type AsyncOverlay =
| Readonly<{
refreshing: false;
staleDegraded: false;
mutationPending: false;
mutationEffectUnknown: false;
mutationConflict: false;
}>
| Readonly<{
refreshing: true;
staleDegraded: false;
mutationPending: false;
mutationEffectUnknown: false;
mutationConflict: false;
}>
| Readonly<{
refreshing: false;
staleDegraded: true;
mutationPending: false;
mutationEffectUnknown: false;
mutationConflict: false;
}>
| Readonly<{
refreshing: false;
staleDegraded: false;
mutationPending: true;
mutationEffectUnknown: false;
mutationConflict: false;
}>
| Readonly<{
refreshing: false;
staleDegraded: false;
mutationPending: false;
mutationEffectUnknown: true;
mutationConflict: false;
}>
| Readonly<{
refreshing: false;
staleDegraded: false;
mutationPending: false;
mutationEffectUnknown: false;
mutationConflict: true;
}>;
export type AsyncSignals = Readonly<{
data?: unknown;
isInitialLoading?: boolean;
failure?: AppFailure;
isFetching?: boolean;
isStale?: boolean;
isDegraded?: boolean;
isMutationPending?: boolean;
hasMutationEffectUnknown?: boolean;
hasMutationConflict?: boolean;
}>;
export type AsyncState = Readonly<{
base: (typeof ASYNC_BASE_STATES)[number];
data?: unknown;
failure?: AppFailure;
overlay: AsyncOverlay;
indicator: (typeof ASYNC_OVERLAYS)[number] | null;
}>;
export function deriveAsyncState(signals: AsyncSignals): AsyncState {
const hasData = signals.data !== undefined && signals.data !== null;
const empty =
hasData &&
((Array.isArray(signals.data) && signals.data.length === 0) ||
signals.data === "");
const base =
signals.isInitialLoading && !hasData
? "initial-loading"
: signals.failure && !hasData
? "terminal-error"
: empty
? "empty"
: hasData
? "success"
: "initial-loading";
const indicator =
signals.hasMutationEffectUnknown && hasData
? "mutation-effect-unknown"
: signals.hasMutationConflict && hasData
? "mutation-conflict"
: signals.isMutationPending && hasData
? "mutation-pending"
: signals.isStale && signals.isDegraded && hasData
? "stale-degraded"
: signals.isFetching && hasData
? "refreshing"
: null;
const overlay = overlayFor(indicator);
return Object.freeze({
base,
data: base === "success" || base === "empty" ? signals.data : undefined,
failure: base === "terminal-error" ? signals.failure : undefined,
overlay,
indicator,
});
}
export function selectOverlayIndicator(
overlay: AsyncOverlay,
): AsyncState["indicator"] {
if (overlay.mutationEffectUnknown) return "mutation-effect-unknown";
if (overlay.mutationConflict) return "mutation-conflict";
if (overlay.mutationPending) return "mutation-pending";
if (overlay.staleDegraded) return "stale-degraded";
if (overlay.refreshing) return "refreshing";
return null;
}
function overlayFor(indicator: AsyncState["indicator"]): AsyncOverlay {
if (indicator === "refreshing") {
return Object.freeze({
refreshing: true,
staleDegraded: false,
mutationPending: false,
mutationEffectUnknown: false,
mutationConflict: false,
});
}
if (indicator === "stale-degraded") {
return Object.freeze({
refreshing: false,
staleDegraded: true,
mutationPending: false,
mutationEffectUnknown: false,
mutationConflict: false,
});
}
if (indicator === "mutation-pending") {
return Object.freeze({
refreshing: false,
staleDegraded: false,
mutationPending: true,
mutationEffectUnknown: false,
mutationConflict: false,
});
}
if (indicator === "mutation-effect-unknown") {
return Object.freeze({
refreshing: false,
staleDegraded: false,
mutationPending: false,
mutationEffectUnknown: true,
mutationConflict: false,
});
}
if (indicator === "mutation-conflict") {
return Object.freeze({
refreshing: false,
staleDegraded: false,
mutationPending: false,
mutationEffectUnknown: false,
mutationConflict: true,
});
}
return Object.freeze({
refreshing: false,
staleDegraded: false,
mutationPending: false,
mutationEffectUnknown: false,
mutationConflict: false,
});
}