feat: 기능 추가 과정중
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { normalizeColorSchemePreference } from "./policies/color-scheme.js";
|
||||
import { normalizeColorSchemePreference } from "./policies/color-scheme.ts";
|
||||
import type {
|
||||
ApplicationApi,
|
||||
ApplicationFeatureId,
|
||||
ApplicationFeatureInputs,
|
||||
ColorSchemePreference,
|
||||
RenderFailureReport,
|
||||
RouteChangedReport,
|
||||
} 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";
|
||||
} 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 };
|
||||
|
||||
@@ -16,7 +18,7 @@ export type { ApplicationApi, ApplicationOutputPorts };
|
||||
*/
|
||||
export function createApplication(
|
||||
outputPorts: ApplicationOutputPorts,
|
||||
featureInputs: Readonly<Record<string, unknown>> = {},
|
||||
featureInputs: Readonly<Partial<ApplicationFeatureInputs>> = {},
|
||||
): ApplicationApi {
|
||||
const session = Object.freeze({
|
||||
getSnapshot: () => outputPorts.session.getState(),
|
||||
@@ -169,14 +171,18 @@ export function createApplication(
|
||||
});
|
||||
const installedFeatureInputs = Object.freeze({ ...featureInputs });
|
||||
const features = Object.freeze({
|
||||
has(featureId: string) {
|
||||
has(featureId: string): featureId is ApplicationFeatureId {
|
||||
return Object.hasOwn(installedFeatureInputs, featureId);
|
||||
},
|
||||
get(featureId: string) {
|
||||
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];
|
||||
return installedFeatureInputs[
|
||||
featureId
|
||||
] as ApplicationFeatureInputs[FeatureId];
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
export const COLOR_SCHEME_PREFERENCES = Object.freeze([
|
||||
"system",
|
||||
"light",
|
||||
"dark",
|
||||
]);
|
||||
|
||||
/** @param {unknown} value */
|
||||
export function normalizeColorSchemePreference(value) {
|
||||
return COLOR_SCHEME_PREFERENCES.includes(/** @type {string} */ (value))
|
||||
? /** @type {"system" | "light" | "dark"} */ (value)
|
||||
: "system";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {"system" | "light" | "dark"} preference
|
||||
* @param {boolean} systemPrefersDark
|
||||
*/
|
||||
export function resolveColorScheme(preference, systemPrefersDark) {
|
||||
if (preference === "system") return systemPrefersDark ? "dark" : "light";
|
||||
return preference;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+36
-31
@@ -4,10 +4,22 @@ export const COMPATIBILITY_TUPLE_FIELDS = Object.freeze([
|
||||
"apiContractVersion",
|
||||
"assetManifestHash",
|
||||
"releaseId",
|
||||
]);
|
||||
] as const);
|
||||
|
||||
/** @param {string} version */
|
||||
export function parseNumericVersion(version) {
|
||||
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 {
|
||||
@@ -17,8 +29,10 @@ export function parseNumericVersion(version) {
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {string} supported @param {string} actual */
|
||||
export function isVersionCompatible(supported, actual) {
|
||||
export function isVersionCompatible(
|
||||
supported: string,
|
||||
actual: string,
|
||||
): boolean {
|
||||
const expected = parseNumericVersion(supported);
|
||||
const candidate = parseNumericVersion(actual);
|
||||
if (!expected || !candidate) return false;
|
||||
@@ -28,26 +42,11 @@ export function isVersionCompatible(supported, actual) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* frontend: {
|
||||
* buildId: string,
|
||||
* configSchemaVersion: string,
|
||||
* apiContractVersion: string,
|
||||
* assetManifestHash: string,
|
||||
* releaseId: string
|
||||
* },
|
||||
* runtime: {
|
||||
* buildId: string,
|
||||
* configSchemaVersion: string,
|
||||
* apiContractVersion: string,
|
||||
* assetManifestHash: string,
|
||||
* releaseId: string
|
||||
* }
|
||||
* }} input
|
||||
*/
|
||||
export function verifyCompatibilityTuple(input) {
|
||||
const mismatches = [];
|
||||
export function verifyCompatibilityTuple(input: Readonly<{
|
||||
frontend: CompatibilityTuple;
|
||||
runtime: CompatibilityTuple;
|
||||
}>) {
|
||||
const mismatches: CompatibilityTupleField[] = [];
|
||||
if (input.frontend.buildId !== input.runtime.buildId) mismatches.push("buildId");
|
||||
if (
|
||||
!isVersionCompatible(
|
||||
@@ -69,7 +68,7 @@ export function verifyCompatibilityTuple(input) {
|
||||
mismatches.push("assetManifestHash");
|
||||
}
|
||||
|
||||
const releaseWarning =
|
||||
const releaseWarning: "releaseId" | null =
|
||||
input.frontend.releaseId === input.runtime.releaseId
|
||||
? null
|
||||
: "releaseId";
|
||||
@@ -80,11 +79,17 @@ export function verifyCompatibilityTuple(input) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ required?: string[], properties?: Record<string, unknown> }} before
|
||||
* @param {{ required?: string[], properties?: Record<string, unknown> }} after
|
||||
*/
|
||||
export function classifyObjectSchemaChange(before, after) {
|
||||
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(
|
||||
+60
-38
@@ -1,11 +1,17 @@
|
||||
/**
|
||||
* @param {{
|
||||
* initialJsGzipBytes: number,
|
||||
* lazyChunks: Array<{ path: string, gzipBytes: number }>
|
||||
* }} measurements
|
||||
* @param {{ initialJsGzipBytes: number, lazyChunkGzipBytes: number }} thresholds
|
||||
*/
|
||||
export function evaluateBundleBudget(measurements, thresholds) {
|
||||
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) => ({
|
||||
@@ -20,14 +26,19 @@ export function evaluateBundleBudget(measurements, thresholds) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* context?: Record<string, unknown>,
|
||||
* metrics: { lcpMs: number, cls: number, namedInteractionMs: number }
|
||||
* }} report
|
||||
* @param {{ lcpMs: number, cls: number, namedInteractionMs: number }} thresholds
|
||||
*/
|
||||
export function evaluateLabBudget(report, thresholds) {
|
||||
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",
|
||||
@@ -53,44 +64,55 @@ export function evaluateLabBudget(report, thresholds) {
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {number[]} values */
|
||||
export function percentile75(values) {
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* metrics: { p75LcpMs: number | null, p75Cls: number | null, p75InpMs: number | null },
|
||||
* eligibleSamples: number
|
||||
* }} report
|
||||
* @param {{
|
||||
* p75LcpMs: number,
|
||||
* p75Cls: number,
|
||||
* p75InpMs: number,
|
||||
* minimumEligibleSamples: number | null
|
||||
* }} thresholds
|
||||
*/
|
||||
export function evaluateFieldBudget(report, thresholds) {
|
||||
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: /** @type {const} */ ("FAIL_UNVERIFIED"),
|
||||
status: "FAIL_UNVERIFIED",
|
||||
passed: false,
|
||||
});
|
||||
}
|
||||
const metrics = report.metrics as Readonly<{
|
||||
p75LcpMs: number;
|
||||
p75Cls: number;
|
||||
p75InpMs: number;
|
||||
}>;
|
||||
const passed =
|
||||
/** @type {number} */ (report.metrics.p75LcpMs) <= thresholds.p75LcpMs &&
|
||||
/** @type {number} */ (report.metrics.p75Cls) <= thresholds.p75Cls &&
|
||||
/** @type {number} */ (report.metrics.p75InpMs) <= thresholds.p75InpMs;
|
||||
metrics.p75LcpMs <= thresholds.p75LcpMs &&
|
||||
metrics.p75Cls <= thresholds.p75Cls &&
|
||||
metrics.p75InpMs <= thresholds.p75InpMs;
|
||||
return Object.freeze({
|
||||
status: passed
|
||||
? /** @type {const} */ ("PASS")
|
||||
: /** @type {const} */ ("FAIL_THRESHOLD"),
|
||||
status: passed ? "PASS" : "FAIL_THRESHOLD",
|
||||
passed,
|
||||
});
|
||||
}
|
||||
+6
-4
@@ -33,10 +33,12 @@ export const PROMOTION_FORMULA = Object.freeze({
|
||||
DOCUMENTATION_READY: Object.freeze(["FE-GATE-017"]),
|
||||
});
|
||||
|
||||
/** @param {Record<string, "PASS" | "FAIL" | "UNVERIFIED">} gateResults */
|
||||
export function evaluatePromotionReadiness(gateResults) {
|
||||
/** @param {readonly string[]} gateIds */
|
||||
const allPass = (gateIds) =>
|
||||
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);
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* @typedef {"authenticated" | "unauthenticated" | "recovery-pending" | "integration-failed"} SessionState
|
||||
*/
|
||||
|
||||
/**
|
||||
* The session is opaque: credentials are attached without exposing tokens.
|
||||
*
|
||||
* @typedef {{
|
||||
* getState(): SessionState,
|
||||
* subscribe(listener: () => void): () => void,
|
||||
* beginSignIn(returnTo?: string): Promise<void>,
|
||||
* signOut(): Promise<void>,
|
||||
* recover(): Promise<"restored" | "no-session">
|
||||
* }} SessionGateway
|
||||
*/
|
||||
|
||||
/**
|
||||
* Credential attachment is an HTTP-adapter collaboration, not an application
|
||||
* input capability.
|
||||
*
|
||||
* @typedef {{
|
||||
* attach(request: Request): Promise<Request>,
|
||||
* onUnauthenticated(): void
|
||||
* }} CredentialAttacher
|
||||
*/
|
||||
|
||||
/**
|
||||
* External auth adapters implement both segregated capabilities.
|
||||
*
|
||||
* @typedef {SessionGateway & CredentialAttacher} AuthSessionPort
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -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" }>>>;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @typedef {{
|
||||
* now(): number,
|
||||
* sleep(milliseconds: number, signal?: AbortSignal): Promise<void>
|
||||
* }} ClockPort
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type ClockPort = Readonly<{
|
||||
now(): number;
|
||||
sleep(milliseconds: number, signal?: AbortSignal): Promise<void>;
|
||||
}>;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DiagnosticRecordInput } from "../../contracts/diagnostics.js";
|
||||
import type { DiagnosticRecordInput } from "../../contracts/diagnostics.ts";
|
||||
|
||||
export type DiagnosticsPort = Readonly<{
|
||||
record(input: DiagnosticRecordInput): void;
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import type { SessionState } from "../auth-session-port.js";
|
||||
import type { StoragePort } from "../storage-port.js";
|
||||
import type { SessionState } from "../auth-session-port.ts";
|
||||
import type { StoragePort } from "../storage-port.ts";
|
||||
|
||||
export type { SessionState } from "../auth-session-port.js";
|
||||
export type { SessionState } from "../auth-session-port.ts";
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
@@ -54,7 +65,9 @@ export type ApplicationApi = Readonly<{
|
||||
>;
|
||||
}>;
|
||||
features: Readonly<{
|
||||
has(featureId: string): boolean;
|
||||
get(featureId: string): unknown;
|
||||
has(featureId: string): featureId is ApplicationFeatureId;
|
||||
get<FeatureId extends ApplicationFeatureId>(
|
||||
featureId: FeatureId,
|
||||
): ApplicationFeatureInputs[FeatureId];
|
||||
}>;
|
||||
}>;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export type {
|
||||
ApplicationApi,
|
||||
ApplicationFeatureId,
|
||||
ApplicationFeatureInputs,
|
||||
ColorSchemePreference,
|
||||
ReleaseSummary,
|
||||
RenderFailureReport,
|
||||
SessionState,
|
||||
} from "./application-api.js";
|
||||
} from "./application-api.ts";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AuthSessionPort } from "../auth-session-port.js";
|
||||
import type { ReleaseInfoPort } from "../release-info-port.js";
|
||||
import type { StoragePort } from "../storage-port.js";
|
||||
import type { TelemetryPort } from "../telemetry-port.js";
|
||||
import type { DiagnosticsPort } from "../diagnostics-port.js";
|
||||
import type { AuthSessionPort } from "../auth-session-port.ts";
|
||||
import type { ReleaseInfoPort } from "../release-info-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
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
export type { ApplicationOutputPorts } from "./application-output-ports.js";
|
||||
export type { ApplicationOutputPorts } from "./application-output-ports.ts";
|
||||
export type {
|
||||
AuthSessionPort,
|
||||
CredentialAttacher,
|
||||
SessionGateway,
|
||||
} from "../auth-session-port.js";
|
||||
export type { ClockPort } from "../clock-port.js";
|
||||
export type { QueryCachePort } from "../query-cache-port.js";
|
||||
export type { ReleaseInfoPort } from "../release-info-port.js";
|
||||
export type { StoragePort } from "../storage-port.js";
|
||||
export type { TelemetryPort } from "../telemetry-port.js";
|
||||
export type { DiagnosticsPort } from "../diagnostics-port.js";
|
||||
} 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;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* @typedef {{
|
||||
* read(key: readonly unknown[]): { ok: true, value: unknown } |
|
||||
* { ok: false, error: import("../../contracts/errors.js").ApiFailure },
|
||||
* write(key: readonly unknown[], value: unknown): { ok: true } |
|
||||
* { ok: false, error: import("../../contracts/errors.js").ApiFailure },
|
||||
* invalidate(namespace: readonly unknown[]): Promise<{ ok: true } |
|
||||
* { ok: false, error: import("../../contracts/errors.js").ApiFailure }>
|
||||
* }} QueryCachePort
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
@@ -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 }>;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* @typedef {{
|
||||
* getCurrent(): Promise<{
|
||||
* schemaVersion?: number,
|
||||
* appVersion?: string,
|
||||
* buildId: string,
|
||||
* commitSha?: string,
|
||||
* configSchemaVersion: string,
|
||||
* apiContractVersion: string,
|
||||
* assetManifestHash: string,
|
||||
* releaseId: string,
|
||||
* builtAt?: string,
|
||||
* routeChunks: Record<string, string>
|
||||
* }>,
|
||||
* refresh(): Promise<{
|
||||
* buildId: string,
|
||||
* configSchemaVersion: string,
|
||||
* apiContractVersion: string,
|
||||
* assetManifestHash: string,
|
||||
* releaseId: string,
|
||||
* routeChunks: Record<string, string>
|
||||
* }>
|
||||
* }} ReleaseInfoPort
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,29 @@
|
||||
export type ReleaseInfo = Readonly<{
|
||||
schemaVersion?: number;
|
||||
appVersion?: string;
|
||||
buildId: string;
|
||||
commitSha?: string;
|
||||
configSchemaVersion: string;
|
||||
apiContractVersion: string;
|
||||
assetManifestHash: string;
|
||||
releaseId: string;
|
||||
builtAt?: string;
|
||||
routeChunks: Readonly<Record<string, string>>;
|
||||
}>;
|
||||
|
||||
export type ActiveReleaseInfo = Readonly<
|
||||
Pick<
|
||||
ReleaseInfo,
|
||||
| "buildId"
|
||||
| "configSchemaVersion"
|
||||
| "apiContractVersion"
|
||||
| "assetManifestHash"
|
||||
| "releaseId"
|
||||
| "routeChunks"
|
||||
>
|
||||
>;
|
||||
|
||||
export type ReleaseInfoPort = Readonly<{
|
||||
getCurrent(): Promise<ReleaseInfo>;
|
||||
refresh(): Promise<ActiveReleaseInfo>;
|
||||
}>;
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* @typedef {{
|
||||
* read(logicalName: string): { ok: true, value: unknown } |
|
||||
* { ok: false, error: import("../../contracts/errors.js").ApiFailure },
|
||||
* write(logicalName: string, value: unknown): { ok: true } |
|
||||
* { ok: false, error: import("../../contracts/errors.js").ApiFailure,
|
||||
* fallback?: string },
|
||||
* remove(logicalName: string): { ok: true } |
|
||||
* { ok: false, error: import("../../contracts/errors.js").ApiFailure }
|
||||
* }} StoragePort
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -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;
|
||||
}>;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TelemetryEventName } from "../../contracts/telemetry.js";
|
||||
import type { TelemetryEventName } from "../../contracts/telemetry.ts";
|
||||
|
||||
export type { TelemetryEventName };
|
||||
|
||||
|
||||
@@ -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 }>;
|
||||
+18
-17
@@ -1,23 +1,24 @@
|
||||
import type { StoragePort } from "../ports/storage-port.ts";
|
||||
|
||||
const RECOVERABLE_KINDS = new Set(["CHUNK_LOAD_FAILURE", "DEPLOY_MISMATCH"]);
|
||||
|
||||
/**
|
||||
* @typedef {{action: "reload-once", releasePair: string} |
|
||||
* {action: "support", reason: string}} ChunkRecoveryDecision
|
||||
*/
|
||||
export type ChunkRecoveryDecision =
|
||||
| Readonly<{ action: "reload-once"; releasePair: string }>
|
||||
| Readonly<{ action: "support"; reason: string }>;
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
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" };
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApiFailure } from "../../contracts/errors.js";
|
||||
import type { AppFailure } from "../../contracts/errors.ts";
|
||||
|
||||
export const ASYNC_BASE_STATES = Object.freeze([
|
||||
"initial-loading",
|
||||
@@ -49,7 +49,7 @@ export type AsyncOverlay =
|
||||
export type AsyncSignals = Readonly<{
|
||||
data?: unknown;
|
||||
isInitialLoading?: boolean;
|
||||
failure?: ApiFailure;
|
||||
failure?: AppFailure;
|
||||
isFetching?: boolean;
|
||||
isStale?: boolean;
|
||||
isDegraded?: boolean;
|
||||
@@ -60,7 +60,7 @@ export type AsyncSignals = Readonly<{
|
||||
export type AsyncState = Readonly<{
|
||||
base: (typeof ASYNC_BASE_STATES)[number];
|
||||
data?: unknown;
|
||||
failure?: ApiFailure;
|
||||
failure?: AppFailure;
|
||||
overlay: AsyncOverlay;
|
||||
indicator: (typeof ASYNC_OVERLAYS)[number] | null;
|
||||
}>;
|
||||
|
||||
Reference in New Issue
Block a user