feat: complete TASK-WP0-02 contract baseline
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dada/shared-contracts": "workspace:*",
|
||||
"@fastify/swagger": "9.8.1",
|
||||
"@sinclair/typebox": "0.34.52",
|
||||
"better-sqlite3": "13.0.1",
|
||||
|
||||
+126
-3
@@ -1,17 +1,140 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import {
|
||||
BootstrapResponseSchema,
|
||||
CorrelationIdSchema,
|
||||
ErrorDetailsSchema,
|
||||
ErrorEnvelopeSchema,
|
||||
GenerationErrorCategorySchema,
|
||||
ModelConfigSseEventSchema,
|
||||
ModelRuntimeSseEventSchema,
|
||||
SseEventSchema,
|
||||
StableEngineeringErrorCodeSchema,
|
||||
StateSseEventSchema,
|
||||
isCorrelationId,
|
||||
type BootstrapResponse,
|
||||
} from "@dada/shared-contracts";
|
||||
import swagger from "@fastify/swagger";
|
||||
import Fastify from "fastify";
|
||||
|
||||
export function createApp() {
|
||||
const app = Fastify({ logger: false });
|
||||
import { EventHub } from "./event-hub.js";
|
||||
|
||||
void app.register(swagger, {
|
||||
const defaultBootstrap: BootstrapResponse = {
|
||||
app_version: "0.0.0",
|
||||
dependencies: [],
|
||||
model_summary: {
|
||||
config_set_version: null,
|
||||
configured_default_model_id: null,
|
||||
recommended_model_id: null,
|
||||
runtime_availability_version: null,
|
||||
},
|
||||
public_features: [],
|
||||
};
|
||||
|
||||
export interface CreateAppOptions {
|
||||
bootstrap?: () => BootstrapResponse | Promise<BootstrapResponse>;
|
||||
eventHub?: EventHub;
|
||||
}
|
||||
|
||||
function requestCorrelationId(headers: Record<string, string | string[] | undefined>) {
|
||||
const header = headers["x-correlation-id"];
|
||||
const candidate = Array.isArray(header) ? header[0] : header;
|
||||
return isCorrelationId(candidate) ? candidate : randomUUID();
|
||||
}
|
||||
|
||||
export async function createApp(options: CreateAppOptions = {}) {
|
||||
const eventHub = options.eventHub ?? new EventHub();
|
||||
const bootstrap = options.bootstrap ?? (() => defaultBootstrap);
|
||||
const app = Fastify({
|
||||
genReqId: (request) => requestCorrelationId(request.headers),
|
||||
logger: false,
|
||||
});
|
||||
|
||||
await app.register(swagger, {
|
||||
openapi: {
|
||||
info: {
|
||||
title: "Dada P0-A",
|
||||
version: "0.0.0",
|
||||
},
|
||||
openapi: "3.1.0",
|
||||
},
|
||||
refResolver: {
|
||||
buildLocalReference: (schema, _baseUri, _fragment, index) =>
|
||||
typeof schema.$id === "string" ? schema.$id : `schema-${index}`,
|
||||
},
|
||||
});
|
||||
|
||||
for (const schema of [
|
||||
CorrelationIdSchema,
|
||||
GenerationErrorCategorySchema,
|
||||
StableEngineeringErrorCodeSchema,
|
||||
ErrorDetailsSchema,
|
||||
ErrorEnvelopeSchema,
|
||||
BootstrapResponseSchema,
|
||||
StateSseEventSchema,
|
||||
ModelConfigSseEventSchema,
|
||||
ModelRuntimeSseEventSchema,
|
||||
SseEventSchema,
|
||||
]) {
|
||||
app.addSchema(schema);
|
||||
}
|
||||
|
||||
app.addHook("onRequest", (request, reply, done) => {
|
||||
reply.header("X-Correlation-Id", request.id);
|
||||
done();
|
||||
});
|
||||
|
||||
app.get(
|
||||
"/api/v1/bootstrap",
|
||||
{
|
||||
schema: {
|
||||
operationId: "getBootstrap",
|
||||
response: {
|
||||
200: BootstrapResponseSchema,
|
||||
},
|
||||
tags: ["Bootstrap"],
|
||||
},
|
||||
},
|
||||
async () => bootstrap(),
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/v1/events",
|
||||
{
|
||||
schema: {
|
||||
operationId: "getEvents",
|
||||
response: {
|
||||
200: {
|
||||
content: {
|
||||
"text/event-stream": {
|
||||
schema: SseEventSchema,
|
||||
},
|
||||
},
|
||||
description: "Non-sensitive state change hints. REST remains authoritative.",
|
||||
},
|
||||
},
|
||||
tags: ["State events"],
|
||||
},
|
||||
},
|
||||
(request, reply) => {
|
||||
reply.hijack();
|
||||
reply.raw.setHeader("Cache-Control", "no-store");
|
||||
reply.raw.setHeader("Connection", "keep-alive");
|
||||
reply.raw.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
||||
reply.raw.setHeader("X-Correlation-Id", request.id);
|
||||
reply.raw.writeHead(200);
|
||||
reply.raw.write(": connected\n\n");
|
||||
|
||||
const unsubscribe = eventHub.connect(
|
||||
(event) => {
|
||||
reply.raw.write(`id: ${event.event_id}\n`);
|
||||
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
|
||||
},
|
||||
() => reply.raw.end(),
|
||||
);
|
||||
request.raw.once("close", unsubscribe);
|
||||
},
|
||||
);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { isSseEvent, type SseEvent } from "@dada/shared-contracts";
|
||||
|
||||
interface Connection {
|
||||
close: () => void;
|
||||
send: (event: SseEvent) => void;
|
||||
}
|
||||
|
||||
export class EventHub {
|
||||
readonly #connections = new Set<Connection>();
|
||||
|
||||
get subscriberCount() {
|
||||
return this.#connections.size;
|
||||
}
|
||||
|
||||
connect(send: Connection["send"], close: Connection["close"]) {
|
||||
const connection = { close, send };
|
||||
this.#connections.add(connection);
|
||||
return () => this.#connections.delete(connection);
|
||||
}
|
||||
|
||||
publish(event: SseEvent) {
|
||||
if (!isSseEvent(event)) throw new Error("SSE event does not match the frozen non-sensitive schema.");
|
||||
for (const connection of this.#connections) connection.send(event);
|
||||
}
|
||||
|
||||
disconnectAll() {
|
||||
const connections = [...this.#connections];
|
||||
this.#connections.clear();
|
||||
for (const connection of connections) connection.close();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createApp } from "./app.js";
|
||||
|
||||
const app = createApp();
|
||||
const app = await createApp();
|
||||
|
||||
await app.listen({
|
||||
host: "127.0.0.1",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dada/shared-contracts": "workspace:*",
|
||||
"@vibrant/core": "4.0.4",
|
||||
"@vibrant/quantizer-mmcq": "4.0.4",
|
||||
"fabric": "7.4.0",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { isSseEvent, type SseEvent } from "@dada/shared-contracts";
|
||||
|
||||
export interface EventSyncDependencies {
|
||||
bootstrap: () => Promise<unknown>;
|
||||
refetchEntity: (entityRef: string) => Promise<unknown>;
|
||||
refetchModels: (input: {
|
||||
configSetVersion?: number;
|
||||
reason: "config" | "runtime";
|
||||
runtimeAvailabilityVersion?: number;
|
||||
}) => Promise<unknown>;
|
||||
}
|
||||
|
||||
export interface EventSyncController {
|
||||
handleDisconnect: () => Promise<void>;
|
||||
handleEvent: (event: unknown) => Promise<void>;
|
||||
}
|
||||
|
||||
async function refetchEventTruth(dependencies: EventSyncDependencies, event: SseEvent) {
|
||||
if ("config_set_version" in event) {
|
||||
await dependencies.refetchModels({
|
||||
configSetVersion: event.config_set_version,
|
||||
reason: "config",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if ("runtime_availability_version" in event) {
|
||||
await dependencies.refetchModels({
|
||||
reason: "runtime",
|
||||
runtimeAvailabilityVersion: event.runtime_availability_version,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await dependencies.refetchEntity(event.entity_ref);
|
||||
}
|
||||
|
||||
export function createEventSyncController(dependencies: EventSyncDependencies): EventSyncController {
|
||||
let lastEventId: number | undefined;
|
||||
let recoveryInProgress = false;
|
||||
|
||||
async function recoverOnce() {
|
||||
if (recoveryInProgress) return;
|
||||
recoveryInProgress = true;
|
||||
await dependencies.bootstrap();
|
||||
}
|
||||
|
||||
return {
|
||||
async handleDisconnect() {
|
||||
await recoverOnce();
|
||||
},
|
||||
async handleEvent(event: unknown) {
|
||||
if (!isSseEvent(event)) throw new Error("SSE event failed the non-sensitive schema.");
|
||||
|
||||
if (lastEventId !== undefined && event.event_id !== lastEventId + 1) {
|
||||
await recoverOnce();
|
||||
}
|
||||
lastEventId = event.event_id;
|
||||
|
||||
await refetchEventTruth(dependencies, event);
|
||||
|
||||
recoveryInProgress = false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function connectEventSource(url: string, controller: EventSyncController) {
|
||||
const source = new EventSource(url, { withCredentials: true });
|
||||
source.onmessage = (message) => {
|
||||
void controller.handleEvent(JSON.parse(message.data) as SseEvent);
|
||||
};
|
||||
source.onerror = () => {
|
||||
void controller.handleDisconnect();
|
||||
};
|
||||
return source;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Generated from openapi/openapi.json. Do not edit by hand.
|
||||
|
||||
export interface ClientOptions { baseUrl?: string; fetch?: typeof globalThis.fetch; headers?: HeadersInit; }
|
||||
|
||||
export async function getBootstrap(options: ClientOptions = {}): Promise<{
|
||||
"app_version": string;
|
||||
"dependencies": Array<{
|
||||
"service_id": string;
|
||||
"status": "available" | "paused" | "degraded" | "unavailable";
|
||||
}>;
|
||||
"model_summary": {
|
||||
"config_set_version": number | null;
|
||||
"configured_default_model_id": string | null;
|
||||
"recommended_model_id": string | null;
|
||||
"runtime_availability_version": number | null;
|
||||
};
|
||||
"public_features": Array<{
|
||||
"feature_id": string;
|
||||
"status": "enabled" | "disabled" | "paused";
|
||||
}>;
|
||||
}> {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
const response = await request(`${options.baseUrl ?? ""}/api/v1/bootstrap`, { method: "GET", headers: options.headers ?? {} });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json() as Promise<{
|
||||
"app_version": string;
|
||||
"dependencies": Array<{
|
||||
"service_id": string;
|
||||
"status": "available" | "paused" | "degraded" | "unavailable";
|
||||
}>;
|
||||
"model_summary": {
|
||||
"config_set_version": number | null;
|
||||
"configured_default_model_id": string | null;
|
||||
"recommended_model_id": string | null;
|
||||
"runtime_availability_version": number | null;
|
||||
};
|
||||
"public_features": Array<{
|
||||
"feature_id": string;
|
||||
"status": "enabled" | "disabled" | "paused";
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function getEvents(options: Pick<ClientOptions, "baseUrl"> = {}): string {
|
||||
return `${options.baseUrl ?? ""}/api/v1/events`;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Generated from openapi/openapi.json. Do not edit by hand.
|
||||
|
||||
export type BootstrapResponse = {
|
||||
"app_version": string;
|
||||
"dependencies": Array<{
|
||||
"service_id": string;
|
||||
"status": "available" | "paused" | "degraded" | "unavailable";
|
||||
}>;
|
||||
"model_summary": {
|
||||
"config_set_version": number | null;
|
||||
"configured_default_model_id": string | null;
|
||||
"recommended_model_id": string | null;
|
||||
"runtime_availability_version": number | null;
|
||||
};
|
||||
"public_features": Array<{
|
||||
"feature_id": string;
|
||||
"status": "enabled" | "disabled" | "paused";
|
||||
}>;
|
||||
};
|
||||
|
||||
export type CorrelationId = string;
|
||||
|
||||
export type ErrorDetails = {
|
||||
"capacity_status"?: "normal" | "warning" | "critical" | "full" | "unavailable";
|
||||
"current_task_ref"?: string;
|
||||
"field_errors"?: Array<{
|
||||
"field": string;
|
||||
"message_key": string;
|
||||
}>;
|
||||
"latest_version"?: number | string;
|
||||
"remaining_bytes"?: number;
|
||||
};
|
||||
|
||||
export type ErrorEnvelope = {
|
||||
"error": {
|
||||
"code": "BROWSER_UNSUPPORTED" | "MODEL_CONFIG_VERSION_CONFLICT" | "MODEL_DEFAULT_REPLACEMENT_REQUIRED" | "MODEL_DEFAULT_REPLACEMENT_INVALID" | "MODEL_RECOMMENDATION_PRIORITY_INVALID" | "MODEL_RECOMMENDATION_PRIORITY_CONFLICT" | "PRIVATE_CONTENT_NOTICE_ACK_REQUIRED" | "ASSET_HISTORY_REFERENCE_CONFLICT" | "ASSET_CLEANUP_CANDIDATE_STALE" | "STORAGE_CAPACITY_EXCEEDED";
|
||||
"correlation_id": string;
|
||||
"details": {
|
||||
"capacity_status"?: "normal" | "warning" | "critical" | "full" | "unavailable";
|
||||
"current_task_ref"?: string;
|
||||
"field_errors"?: Array<{
|
||||
"field": string;
|
||||
"message_key": string;
|
||||
}>;
|
||||
"latest_version"?: number | string;
|
||||
"remaining_bytes"?: number;
|
||||
};
|
||||
"error_category"?: "upstream_timeout" | "upstream_failed" | "safety_rejected" | "model_disabled" | "gateway_balance_insufficient" | "gateway_contract_invalid" | "reference_invalid" | "unknown_retryable" | "unknown_non_retryable";
|
||||
"message_key": string;
|
||||
};
|
||||
};
|
||||
|
||||
export type GenerationErrorCategory = "upstream_timeout" | "upstream_failed" | "safety_rejected" | "model_disabled" | "gateway_balance_insufficient" | "gateway_contract_invalid" | "reference_invalid" | "unknown_retryable" | "unknown_non_retryable";
|
||||
|
||||
export type ModelConfigSseEvent = {
|
||||
"config_set_version": number;
|
||||
"entity_ref": string;
|
||||
"event_id": number;
|
||||
"event_type": "model_config_changed";
|
||||
"occurred_at": string;
|
||||
};
|
||||
|
||||
export type ModelRuntimeSseEvent = {
|
||||
"entity_ref": string;
|
||||
"event_id": number;
|
||||
"event_type": "model_runtime_changed";
|
||||
"occurred_at": string;
|
||||
"runtime_availability_version": number;
|
||||
};
|
||||
|
||||
export type SseEvent = {
|
||||
"entity_ref": string;
|
||||
"event_id": number;
|
||||
"event_type": string;
|
||||
"occurred_at": string;
|
||||
"state_version": number;
|
||||
} | {
|
||||
"config_set_version": number;
|
||||
"entity_ref": string;
|
||||
"event_id": number;
|
||||
"event_type": "model_config_changed";
|
||||
"occurred_at": string;
|
||||
} | {
|
||||
"entity_ref": string;
|
||||
"event_id": number;
|
||||
"event_type": "model_runtime_changed";
|
||||
"occurred_at": string;
|
||||
"runtime_availability_version": number;
|
||||
};
|
||||
|
||||
export type StableEngineeringErrorCode = "BROWSER_UNSUPPORTED" | "MODEL_CONFIG_VERSION_CONFLICT" | "MODEL_DEFAULT_REPLACEMENT_REQUIRED" | "MODEL_DEFAULT_REPLACEMENT_INVALID" | "MODEL_RECOMMENDATION_PRIORITY_INVALID" | "MODEL_RECOMMENDATION_PRIORITY_CONFLICT" | "PRIVATE_CONTENT_NOTICE_ACK_REQUIRED" | "ASSET_HISTORY_REFERENCE_CONFLICT" | "ASSET_CLEANUP_CANDIDATE_STALE" | "STORAGE_CAPACITY_EXCEEDED";
|
||||
|
||||
export type StateSseEvent = {
|
||||
"entity_ref": string;
|
||||
"event_id": number;
|
||||
"event_type": string;
|
||||
"occurred_at": string;
|
||||
"state_version": number;
|
||||
};
|
||||
Reference in New Issue
Block a user