chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
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],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user