Files
tech-log-frontend/src/contracts/runtime-capabilities.ts
T

267 lines
8.7 KiB
TypeScript

import type { InstalledOfflineCommandContribution } from "./offline-command.ts";
import type { InstalledServiceWorkerSelection } from "./service-worker.ts";
import type { InstalledWebWorkerContribution } from "./web-worker.ts";
/**
* §3.4–§3.6. Optional runtime capability selection and hosting.
*
* Source contribution decides what is installed. Runtime Config may only carry
* `DEFAULT | DISABLED`, so a configuration document can never switch on a
* capability whose source is absent.
*/
export type RuntimeCapabilityOverride = "DEFAULT" | "DISABLED";
export type RuntimeStopReason =
| "APPLICATION_SHUTDOWN"
| "SCOPE_FENCED"
| "FEATURE_DISABLED"
| "HIDDEN_POLICY"
| "INCIDENT_CONTAINMENT";
export interface RuntimeLifecycle {
start(): void | Promise<void>;
stop(reason: RuntimeStopReason): void | Promise<void>;
dispose(): void | Promise<void>;
}
/** §20.2. `FAILED -> STARTING` is never automatic. */
export type RuntimeLifecycleState =
| "NEW"
| "STARTING"
| "RUNNING"
| "STOPPING"
| "STOPPED"
| "FAILED"
| "DISPOSING"
| "DISPOSED";
/** §22.12. Health is per capability; there is no global `healthy` boolean. */
export type RuntimeHealth =
| "AVAILABLE"
| "DEGRADED"
| "UNAVAILABLE"
| "INCOMPATIBLE"
| "DISABLED";
// §13.2. Realtime product contribution.
export type RealtimeEffectKind =
| "INVALIDATE_TOPICS"
| "APPLY_AUTHORITATIVE_DELTA"
| "EPHEMERAL_NOTIFICATION";
export interface InstalledRealtimeEventEffect {
readonly eventType: string;
readonly mapperId: string;
readonly effect: RealtimeEffectKind;
readonly invalidationTopics: readonly string[];
}
export type InstalledRealtimeTransport =
| Readonly<{ kind: "SSE"; endpointId: string }>
| Readonly<{ kind: "WEBSOCKET"; endpointId: string }>
| Readonly<{ kind: "POLLING"; operationId: string; intervalMs: number }>;
export interface InstalledRealtimeContribution {
readonly contributionId: string;
readonly featureId: string;
readonly contractSourcePackageId: string;
readonly streamId: string;
readonly recoveryMode: "CURSOR" | "SNAPSHOT_ONLY" | "SESSION_REBUILD";
readonly eventEffects: readonly InstalledRealtimeEventEffect[];
readonly transport: InstalledRealtimeTransport;
}
export const REALTIME_CONTRIBUTION_BOUNDS = Object.freeze({
contributions: 64,
streams: 128,
eventTypes: 512,
effectsPerEvent: 8,
invalidationTopicsPerEvent: 32,
minimumPollingIntervalMs: 5_000,
maximumPollingIntervalMs: 300_000,
});
export interface InstalledRuntimeCapabilities {
readonly realtime: readonly InstalledRealtimeContribution[];
readonly webWorkers: readonly InstalledWebWorkerContribution[];
readonly serviceWorker: InstalledServiceWorkerSelection | null;
readonly offlineCommands: InstalledOfflineCommandContribution | null;
}
export type CapabilityOverrideMap = Readonly<{
REALTIME: RuntimeCapabilityOverride;
WEB_WORKER: RuntimeCapabilityOverride;
SERVICE_WORKER: RuntimeCapabilityOverride;
OFFLINE_COMMANDS: RuntimeCapabilityOverride;
}>;
/**
* The effective selection after applying runtime overrides. `serviceWorkerMode`
* keeps the §3.6 persistent-registration exception explicit: a statically
* `ACTIVE` worker that runtime config disables still performs exactly one
* owned-registration lookup and at most one unregister, and deletes no cache.
*/
export type ResolvedRuntimeCapabilities = Readonly<{
realtime: readonly InstalledRealtimeContribution[];
webWorkers: readonly InstalledWebWorkerContribution[];
serviceWorker: InstalledServiceWorkerSelection | null;
serviceWorkerDisabledCleanup: boolean;
offlineCommands: InstalledOfflineCommandContribution | null;
}>;
export function resolveRuntimeCapabilities(
installed: InstalledRuntimeCapabilities,
overrides: CapabilityOverrideMap,
): ResolvedRuntimeCapabilities {
const realtimeDisabled = overrides.REALTIME === "DISABLED";
const workersDisabled = overrides.WEB_WORKER === "DISABLED";
const serviceWorkerDisabled = overrides.SERVICE_WORKER === "DISABLED";
const offlineDisabled = overrides.OFFLINE_COMMANDS === "DISABLED";
return Object.freeze({
realtime: realtimeDisabled ? Object.freeze([]) : installed.realtime,
webWorkers: workersDisabled ? Object.freeze([]) : installed.webWorkers,
serviceWorker: serviceWorkerDisabled ? null : installed.serviceWorker,
serviceWorkerDisabledCleanup:
serviceWorkerDisabled && installed.serviceWorker?.mode === "ACTIVE",
offlineCommands: offlineDisabled ? null : installed.offlineCommands,
});
}
export function validateRealtimeContributions(
contributions: readonly InstalledRealtimeContribution[],
): readonly InstalledRealtimeContribution[] {
const bounds = REALTIME_CONTRIBUTION_BOUNDS;
if (contributions.length > bounds.contributions) {
throw new TypeError("Realtime contributions exceed their bound.");
}
const contributionIds = new Set<string>();
const streamIds = new Set<string>();
let eventTypeCount = 0;
for (const contribution of contributions) {
if (
!contribution.contributionId ||
contributionIds.has(contribution.contributionId)
) {
throw new TypeError("Duplicate realtime contribution identity.");
}
contributionIds.add(contribution.contributionId);
streamIds.add(contribution.streamId);
if (streamIds.size > bounds.streams) {
throw new TypeError("Realtime streams exceed their bound.");
}
const transport = contribution.transport;
if (transport.kind === "POLLING") {
if (
!Number.isSafeInteger(transport.intervalMs) ||
transport.intervalMs < bounds.minimumPollingIntervalMs ||
transport.intervalMs > bounds.maximumPollingIntervalMs
) {
throw new TypeError(
`Realtime polling interval is out of range: ${contribution.contributionId}`,
);
}
}
const seenEvents = new Map<string, number>();
for (const effect of contribution.eventEffects) {
eventTypeCount += 1;
if (eventTypeCount > bounds.eventTypes) {
throw new TypeError("Realtime event types exceed their bound.");
}
const count = (seenEvents.get(effect.eventType) ?? 0) + 1;
if (count > bounds.effectsPerEvent) {
throw new TypeError(
`Realtime effects per event exceeded: ${effect.eventType}`,
);
}
seenEvents.set(effect.eventType, count);
if (
effect.invalidationTopics.length > bounds.invalidationTopicsPerEvent
) {
throw new TypeError(
`Realtime invalidation topics exceeded: ${effect.eventType}`,
);
}
if (
effect.effect === "INVALIDATE_TOPICS" &&
effect.invalidationTopics.length === 0
) {
throw new TypeError(
`INVALIDATE_TOPICS effect declares no topic: ${effect.eventType}`,
);
}
}
}
return Object.freeze([...contributions]);
}
export type RuntimeCapabilityId =
| "REALTIME"
| "WEB_WORKER"
| "SERVICE_WORKER"
| "OFFLINE_COMMANDS";
/**
* §3.5. A bounded, serialisable view of one capability. `selected` is the
* static SSOT count and `active` is what survived the runtime override, so the
* difference between the two is exactly the operator's effect. An override can
* only subtract, which is why a never-selected capability stays at zero.
*/
export type RuntimeCapabilityStatus = Readonly<{
capabilityId: RuntimeCapabilityId;
selected: number;
active: number;
override: RuntimeCapabilityOverride;
}>;
export type RuntimeCapabilitySnapshot = readonly RuntimeCapabilityStatus[];
const CAPABILITY_ORDER = Object.freeze([
"REALTIME",
"WEB_WORKER",
"SERVICE_WORKER",
"OFFLINE_COMMANDS",
] as const);
export function describeRuntimeCapabilities(
installed: InstalledRuntimeCapabilities,
overrides: CapabilityOverrideMap,
): RuntimeCapabilitySnapshot {
const resolved = resolveRuntimeCapabilities(installed, overrides);
const counts: Readonly<
Record<RuntimeCapabilityId, Readonly<{ selected: number; active: number }>>
> = Object.freeze({
REALTIME: Object.freeze({
selected: installed.realtime.length,
active: resolved.realtime.length,
}),
WEB_WORKER: Object.freeze({
selected: installed.webWorkers.length,
active: resolved.webWorkers.length,
}),
SERVICE_WORKER: Object.freeze({
selected: installed.serviceWorker === null ? 0 : 1,
active: resolved.serviceWorker === null ? 0 : 1,
}),
OFFLINE_COMMANDS: Object.freeze({
selected: installed.offlineCommands === null ? 0 : 1,
active: resolved.offlineCommands === null ? 0 : 1,
}),
});
return Object.freeze(
CAPABILITY_ORDER.map((capabilityId) =>
Object.freeze({
capabilityId,
selected: counts[capabilityId].selected,
active: counts[capabilityId].active,
override: overrides[capabilityId],
}),
),
);
}