feat(lab): publish E40 perception product gate
This commit is contained in:
@@ -1,3 +1,17 @@
|
||||
import {
|
||||
fetchE34TemporalLayerResult,
|
||||
type E34TemporalLayerResult,
|
||||
} from "./e34TemporalLayer";
|
||||
import {
|
||||
fetchE35DegradationRecoveryResult,
|
||||
type E35DegradationRecoveryResult,
|
||||
} from "./e35DegradationRecovery";
|
||||
import {
|
||||
fetchE40ProductGateResult,
|
||||
type E40PerceptionProductGateResult,
|
||||
} from "./e40ProductGate";
|
||||
import { settledCatalogValue } from "./catalogTransport";
|
||||
|
||||
export interface E31LaboratoryResult {
|
||||
resultId: string;
|
||||
createdAtUtc: string | null;
|
||||
@@ -179,7 +193,7 @@ export interface E38PerceptionBaselineResult {
|
||||
access: "read-only";
|
||||
}
|
||||
|
||||
export interface E39DevelopmentDimensionMetric {
|
||||
export interface DevelopmentDimensionMetric {
|
||||
correct: number;
|
||||
incorrect: number;
|
||||
total: number;
|
||||
@@ -188,6 +202,8 @@ export interface E39DevelopmentDimensionMetric {
|
||||
passed: boolean;
|
||||
}
|
||||
|
||||
export type E39DevelopmentDimensionMetric = DevelopmentDimensionMetric;
|
||||
|
||||
export interface E39PerceptionRefinementResult {
|
||||
resultId: string;
|
||||
createdAtUtc: string | null;
|
||||
@@ -205,9 +221,9 @@ export interface E39PerceptionRefinementResult {
|
||||
validationLabelsUsed: false;
|
||||
passed: boolean;
|
||||
dimensions: {
|
||||
presence: E39DevelopmentDimensionMetric;
|
||||
geometryAssociation: E39DevelopmentDimensionMetric;
|
||||
freshness: E39DevelopmentDimensionMetric;
|
||||
presence: DevelopmentDimensionMetric;
|
||||
geometryAssociation: DevelopmentDimensionMetric;
|
||||
freshness: DevelopmentDimensionMetric;
|
||||
};
|
||||
};
|
||||
metrics: E38PerceptionBaselineResult["metrics"];
|
||||
@@ -230,6 +246,7 @@ export interface AdvancedLaboratoryResults {
|
||||
e37: E37AcceptanceContractResult | null;
|
||||
e38: E38PerceptionBaselineResult | null;
|
||||
e39: E39PerceptionRefinementResult | null;
|
||||
e40: E40PerceptionProductGateResult | null;
|
||||
}
|
||||
|
||||
export class AdvancedLaboratoryContractError extends Error {
|
||||
@@ -778,10 +795,10 @@ function parseE38(value: unknown): E38PerceptionBaselineResult {
|
||||
};
|
||||
}
|
||||
|
||||
function parseE39DevelopmentDimension(
|
||||
function parseDevelopmentDimension(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): E39DevelopmentDimensionMetric {
|
||||
): DevelopmentDimensionMetric {
|
||||
const source = record(value, label);
|
||||
return {
|
||||
correct: integerValue(source.correct, `${label}.correct`),
|
||||
@@ -859,15 +876,15 @@ function parseE39(value: unknown): E39PerceptionRefinementResult {
|
||||
"E39.development_cross_validation.passed",
|
||||
),
|
||||
dimensions: {
|
||||
presence: parseE39DevelopmentDimension(
|
||||
presence: parseDevelopmentDimension(
|
||||
developmentDimensions.presence,
|
||||
"E39.development_cross_validation.dimensions.presence",
|
||||
),
|
||||
geometryAssociation: parseE39DevelopmentDimension(
|
||||
geometryAssociation: parseDevelopmentDimension(
|
||||
developmentDimensions.geometry_association,
|
||||
"E39.development_cross_validation.dimensions.geometry_association",
|
||||
),
|
||||
freshness: parseE39DevelopmentDimension(
|
||||
freshness: parseDevelopmentDimension(
|
||||
developmentDimensions.freshness,
|
||||
"E39.development_cross_validation.dimensions.freshness",
|
||||
),
|
||||
@@ -951,7 +968,7 @@ export async function fetchAdvancedLaboratoryResults({
|
||||
fetcher?: LaboratoryFetch;
|
||||
signal?: AbortSignal;
|
||||
} = {}): Promise<AdvancedLaboratoryResults> {
|
||||
const [e31, e32, e33, e34, e35, e37, e38, e39] = await Promise.all([
|
||||
const settled = await Promise.allSettled([
|
||||
fetchOne("/api/v1/laboratory/e31/results?limit=1", parseE31, fetcher, signal),
|
||||
fetchOne("/api/v1/laboratory/e32/results?limit=1", parseE32, fetcher, signal),
|
||||
fetchOne("/api/v1/laboratory/e33/results?limit=1", parseE33, fetcher, signal),
|
||||
@@ -960,14 +977,16 @@ export async function fetchAdvancedLaboratoryResults({
|
||||
fetchOne("/api/v1/laboratory/e37/results?limit=1", parseE37, fetcher, signal),
|
||||
fetchOne("/api/v1/laboratory/e38/results?limit=1", parseE38, fetcher, signal),
|
||||
fetchOne("/api/v1/laboratory/e39/results?limit=1", parseE39, fetcher, signal),
|
||||
fetchE40ProductGateResult({ fetcher, signal }),
|
||||
]);
|
||||
return { e31, e32, e33, e34, e35, e37, e38, e39 };
|
||||
const e31 = settledCatalogValue(settled[0]);
|
||||
const e32 = settledCatalogValue(settled[1]);
|
||||
const e33 = settledCatalogValue(settled[2]);
|
||||
const e34 = settledCatalogValue(settled[3]);
|
||||
const e35 = settledCatalogValue(settled[4]);
|
||||
const e37 = settledCatalogValue(settled[5]);
|
||||
const e38 = settledCatalogValue(settled[6]);
|
||||
const e39 = settledCatalogValue(settled[7]);
|
||||
const e40 = settledCatalogValue(settled[8]);
|
||||
return { e31, e32, e33, e34, e35, e37, e38, e39, e40 };
|
||||
}
|
||||
import {
|
||||
fetchE34TemporalLayerResult,
|
||||
type E34TemporalLayerResult,
|
||||
} from "./e34TemporalLayer";
|
||||
import {
|
||||
fetchE35DegradationRecoveryResult,
|
||||
type E35DegradationRecoveryResult,
|
||||
} from "./e35DegradationRecovery";
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export function settledCatalogValue<T>(
|
||||
result: PromiseSettledResult<T>,
|
||||
): T | null {
|
||||
if (result.status === "fulfilled") {
|
||||
return result.value;
|
||||
}
|
||||
const reason = result.reason;
|
||||
const isAbort = reason instanceof Error && reason.name === "AbortError";
|
||||
const isHttpTransport = (
|
||||
reason instanceof Error
|
||||
&& /Каталог LAB.*недоступен: HTTP \d+\./.test(reason.message)
|
||||
);
|
||||
if (!isAbort && (reason instanceof TypeError || isHttpTransport)) {
|
||||
return null;
|
||||
}
|
||||
throw reason;
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
export interface E40DevelopmentDimensionMetric {
|
||||
correct: number;
|
||||
incorrect: number;
|
||||
total: number;
|
||||
accuracy: number;
|
||||
target: number;
|
||||
passed: boolean;
|
||||
}
|
||||
|
||||
export interface E40DimensionMetric extends E40DevelopmentDimensionMetric {
|
||||
byStratum: Readonly<Record<string, {
|
||||
correct: number;
|
||||
incorrect: number;
|
||||
total: number;
|
||||
accuracy: number;
|
||||
}>>;
|
||||
confusion: readonly {
|
||||
reference: string;
|
||||
prediction: string;
|
||||
count: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface E40DevelopmentProtocol {
|
||||
items: number;
|
||||
foldSizes: Readonly<Record<string, number>>;
|
||||
passed: boolean;
|
||||
dimensions: {
|
||||
presence: E40DevelopmentDimensionMetric;
|
||||
geometryAssociation: E40DevelopmentDimensionMetric;
|
||||
freshness: E40DevelopmentDimensionMetric;
|
||||
};
|
||||
}
|
||||
|
||||
export interface E40PerceptionProductGateResult {
|
||||
resultId: string;
|
||||
createdAtUtc: string | null;
|
||||
sourceSessionId: string;
|
||||
sourceDisplayName: string;
|
||||
status: "measured-leakage-resistant-product-gate";
|
||||
profileId: string;
|
||||
workerNode: string;
|
||||
qualityGatePassed: boolean;
|
||||
developmentCrossValidation: {
|
||||
strategy: string;
|
||||
seed: string;
|
||||
folds: number;
|
||||
items: number;
|
||||
validationLabelsUsed: false;
|
||||
passed: boolean;
|
||||
protocols: {
|
||||
contiguousSourceTime: E40DevelopmentProtocol;
|
||||
wholeTrackOrSceneWindow: E40DevelopmentProtocol;
|
||||
};
|
||||
};
|
||||
metrics: {
|
||||
developmentItems: number;
|
||||
validationItems: number;
|
||||
terminalOutcomes: number;
|
||||
accountingFraction: number;
|
||||
falseFreeClaims: number;
|
||||
highSeverityFailures: number;
|
||||
dimensions: {
|
||||
presence: E40DimensionMetric;
|
||||
geometryAssociation: E40DimensionMetric;
|
||||
freshness: E40DimensionMetric;
|
||||
};
|
||||
};
|
||||
blockingChecks: readonly string[];
|
||||
method: {
|
||||
summary: string;
|
||||
selection: string;
|
||||
dimensionProjection: string;
|
||||
};
|
||||
limitations: readonly string[];
|
||||
access: "read-only";
|
||||
}
|
||||
|
||||
export class E40ProductGateContractError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "E40ProductGateContractError";
|
||||
}
|
||||
}
|
||||
|
||||
type LaboratoryFetch = (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
) => Promise<Response>;
|
||||
|
||||
function record(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new E40ProductGateContractError(`${label}: ожидался объект.`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown, label: string): string {
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new E40ProductGateContractError(`${label}: ожидалась строка.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalString(value: unknown, label: string): string | null {
|
||||
return value === null ? null : stringValue(value, label);
|
||||
}
|
||||
|
||||
function numberValue(value: unknown, label: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
||||
throw new E40ProductGateContractError(`${label}: ожидалось число.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function integerValue(value: unknown, label: string): number {
|
||||
const parsed = numberValue(value, label);
|
||||
if (!Number.isSafeInteger(parsed)) {
|
||||
throw new E40ProductGateContractError(`${label}: ожидалось целое число.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function booleanValue(value: unknown, label: string): boolean {
|
||||
if (typeof value !== "boolean") {
|
||||
throw new E40ProductGateContractError(`${label}: ожидался boolean.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function falseValue(value: unknown, label: string): false {
|
||||
if (value !== false) {
|
||||
throw new E40ProductGateContractError(`${label}: ожидалось false.`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function exactString<T extends string>(
|
||||
value: unknown,
|
||||
expected: T,
|
||||
label: string,
|
||||
): T {
|
||||
if (value !== expected) {
|
||||
throw new E40ProductGateContractError(`${label}: неверное значение.`);
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
|
||||
function strings(value: unknown, label: string): readonly string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new E40ProductGateContractError(`${label}: ожидался массив.`);
|
||||
}
|
||||
return value.map((item, index) => stringValue(item, `${label}[${index}]`));
|
||||
}
|
||||
|
||||
function numberRecord(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): Readonly<Record<string, number>> {
|
||||
const source = record(value, label);
|
||||
return Object.fromEntries(
|
||||
Object.entries(source).map(([key, item]) => [
|
||||
key,
|
||||
integerValue(item, `${label}.${key}`),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function contentId(value: unknown, prefix: string, label: string): string {
|
||||
const parsed = stringValue(value, label);
|
||||
if (!new RegExp(`^${prefix}-[a-f0-9]{64}$`).test(parsed)) {
|
||||
throw new E40ProductGateContractError(`${label}: неверный content id.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function diagnosticAuthority(value: unknown, label: string): void {
|
||||
const authority = record(value, label);
|
||||
if (
|
||||
authority.commands_enabled !== false
|
||||
|| authority.navigation_or_safety_accepted !== false
|
||||
) {
|
||||
throw new E40ProductGateContractError(`${label}: запрещённые полномочия.`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseDevelopmentDimension(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): E40DevelopmentDimensionMetric {
|
||||
const dimension = record(value, label);
|
||||
return {
|
||||
correct: integerValue(dimension.correct, `${label}.correct`),
|
||||
incorrect: integerValue(dimension.incorrect, `${label}.incorrect`),
|
||||
total: integerValue(dimension.total, `${label}.total`),
|
||||
accuracy: numberValue(dimension.accuracy, `${label}.accuracy`),
|
||||
target: numberValue(dimension.target, `${label}.target`),
|
||||
passed: booleanValue(dimension.passed, `${label}.passed`),
|
||||
};
|
||||
}
|
||||
|
||||
function parseDimension(value: unknown, label: string): E40DimensionMetric {
|
||||
const dimension = record(value, label);
|
||||
const byStratum = record(dimension.by_stratum, `${label}.by_stratum`);
|
||||
const confusion = dimension.confusion;
|
||||
if (!Array.isArray(confusion)) {
|
||||
throw new E40ProductGateContractError(
|
||||
`${label}.confusion: ожидался массив.`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
...parseDevelopmentDimension(value, label),
|
||||
byStratum: Object.fromEntries(
|
||||
Object.entries(byStratum).map(([stratum, raw]) => {
|
||||
const item = record(raw, `${label}.by_stratum.${stratum}`);
|
||||
return [stratum, {
|
||||
correct: integerValue(
|
||||
item.correct,
|
||||
`${label}.by_stratum.${stratum}.correct`,
|
||||
),
|
||||
incorrect: integerValue(
|
||||
item.incorrect,
|
||||
`${label}.by_stratum.${stratum}.incorrect`,
|
||||
),
|
||||
total: integerValue(
|
||||
item.total,
|
||||
`${label}.by_stratum.${stratum}.total`,
|
||||
),
|
||||
accuracy: numberValue(
|
||||
item.accuracy,
|
||||
`${label}.by_stratum.${stratum}.accuracy`,
|
||||
),
|
||||
}];
|
||||
}),
|
||||
),
|
||||
confusion: confusion.map((raw, index) => {
|
||||
const item = record(raw, `${label}.confusion[${index}]`);
|
||||
return {
|
||||
reference: stringValue(
|
||||
item.reference,
|
||||
`${label}.confusion[${index}].reference`,
|
||||
),
|
||||
prediction: stringValue(
|
||||
item.prediction,
|
||||
`${label}.confusion[${index}].prediction`,
|
||||
),
|
||||
count: integerValue(
|
||||
item.count,
|
||||
`${label}.confusion[${index}].count`,
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function parseDevelopmentProtocol(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): E40DevelopmentProtocol {
|
||||
const protocol = record(value, label);
|
||||
const dimensions = record(protocol.dimensions, `${label}.dimensions`);
|
||||
return {
|
||||
items: integerValue(protocol.items, `${label}.items`),
|
||||
foldSizes: numberRecord(protocol.fold_sizes, `${label}.fold_sizes`),
|
||||
passed: booleanValue(protocol.passed, `${label}.passed`),
|
||||
dimensions: {
|
||||
presence: parseDevelopmentDimension(
|
||||
dimensions.presence,
|
||||
`${label}.dimensions.presence`,
|
||||
),
|
||||
geometryAssociation: parseDevelopmentDimension(
|
||||
dimensions.geometry_association,
|
||||
`${label}.dimensions.geometry_association`,
|
||||
),
|
||||
freshness: parseDevelopmentDimension(
|
||||
dimensions.freshness,
|
||||
`${label}.dimensions.freshness`,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function parseResult(value: unknown): E40PerceptionProductGateResult {
|
||||
const item = record(value, "E40");
|
||||
const metrics = record(item.metrics, "E40.metrics");
|
||||
const dimensions = record(metrics.dimensions, "E40.metrics.dimensions");
|
||||
const development = record(
|
||||
item.development_cross_validation,
|
||||
"E40.development_cross_validation",
|
||||
);
|
||||
const protocols = record(
|
||||
development.protocols,
|
||||
"E40.development_cross_validation.protocols",
|
||||
);
|
||||
const method = record(item.method, "E40.method");
|
||||
diagnosticAuthority(item.authority, "E40.authority");
|
||||
return {
|
||||
resultId: contentId(
|
||||
item.result_id,
|
||||
"e40-perception-product-gate",
|
||||
"E40.result_id",
|
||||
),
|
||||
createdAtUtc: optionalString(item.created_at_utc, "E40.created_at_utc"),
|
||||
sourceSessionId: stringValue(
|
||||
item.source_session_id,
|
||||
"E40.source_session_id",
|
||||
),
|
||||
sourceDisplayName: stringValue(
|
||||
item.source_display_name,
|
||||
"E40.source_display_name",
|
||||
),
|
||||
status: exactString(
|
||||
item.status,
|
||||
"measured-leakage-resistant-product-gate",
|
||||
"E40.status",
|
||||
),
|
||||
profileId: stringValue(item.profile_id, "E40.profile_id"),
|
||||
workerNode: stringValue(item.worker_node, "E40.worker_node"),
|
||||
qualityGatePassed: booleanValue(
|
||||
item.quality_gate_passed,
|
||||
"E40.quality_gate_passed",
|
||||
),
|
||||
developmentCrossValidation: {
|
||||
strategy: stringValue(
|
||||
development.strategy,
|
||||
"E40.development_cross_validation.strategy",
|
||||
),
|
||||
seed: stringValue(
|
||||
development.seed,
|
||||
"E40.development_cross_validation.seed",
|
||||
),
|
||||
folds: integerValue(
|
||||
development.folds,
|
||||
"E40.development_cross_validation.folds",
|
||||
),
|
||||
items: integerValue(
|
||||
development.items,
|
||||
"E40.development_cross_validation.items",
|
||||
),
|
||||
validationLabelsUsed: falseValue(
|
||||
development.validation_labels_used,
|
||||
"E40.development_cross_validation.validation_labels_used",
|
||||
),
|
||||
passed: booleanValue(
|
||||
development.passed,
|
||||
"E40.development_cross_validation.passed",
|
||||
),
|
||||
protocols: {
|
||||
contiguousSourceTime: parseDevelopmentProtocol(
|
||||
protocols["contiguous-source-time-five-fold"],
|
||||
"E40.development_cross_validation.protocols.contiguous-source-time-five-fold",
|
||||
),
|
||||
wholeTrackOrSceneWindow: parseDevelopmentProtocol(
|
||||
protocols["whole-track-or-scene-window-five-fold"],
|
||||
"E40.development_cross_validation.protocols.whole-track-or-scene-window-five-fold",
|
||||
),
|
||||
},
|
||||
},
|
||||
metrics: {
|
||||
developmentItems: integerValue(
|
||||
metrics.development_items,
|
||||
"E40.metrics.development_items",
|
||||
),
|
||||
validationItems: integerValue(
|
||||
metrics.validation_items,
|
||||
"E40.metrics.validation_items",
|
||||
),
|
||||
terminalOutcomes: integerValue(
|
||||
metrics.terminal_outcomes,
|
||||
"E40.metrics.terminal_outcomes",
|
||||
),
|
||||
accountingFraction: numberValue(
|
||||
metrics.accounting_fraction,
|
||||
"E40.metrics.accounting_fraction",
|
||||
),
|
||||
falseFreeClaims: integerValue(
|
||||
metrics.false_free_claims,
|
||||
"E40.metrics.false_free_claims",
|
||||
),
|
||||
highSeverityFailures: integerValue(
|
||||
metrics.high_severity_failures,
|
||||
"E40.metrics.high_severity_failures",
|
||||
),
|
||||
dimensions: {
|
||||
presence: parseDimension(
|
||||
dimensions.presence,
|
||||
"E40.metrics.dimensions.presence",
|
||||
),
|
||||
geometryAssociation: parseDimension(
|
||||
dimensions.geometry_association,
|
||||
"E40.metrics.dimensions.geometry_association",
|
||||
),
|
||||
freshness: parseDimension(
|
||||
dimensions.freshness,
|
||||
"E40.metrics.dimensions.freshness",
|
||||
),
|
||||
},
|
||||
},
|
||||
blockingChecks: strings(item.blocking_checks, "E40.blocking_checks"),
|
||||
method: {
|
||||
summary: stringValue(method.summary, "E40.method.summary"),
|
||||
selection: stringValue(method.selection, "E40.method.selection"),
|
||||
dimensionProjection: stringValue(
|
||||
method.dimension_projection,
|
||||
"E40.method.dimension_projection",
|
||||
),
|
||||
},
|
||||
limitations: strings(item.limitations, "E40.limitations"),
|
||||
access: exactString(item.access, "read-only", "E40.access"),
|
||||
};
|
||||
}
|
||||
|
||||
function parseCatalog(payload: unknown): E40PerceptionProductGateResult | null {
|
||||
const catalog = record(payload, "Каталог LAB E40");
|
||||
exactString(
|
||||
catalog.schema_version,
|
||||
"missioncore.laboratory-advanced-catalog/v1",
|
||||
"Каталог LAB E40.schema_version",
|
||||
);
|
||||
booleanValue(catalog.configured, "Каталог LAB E40.configured");
|
||||
integerValue(catalog.candidate_total, "Каталог LAB E40.candidate_total");
|
||||
integerValue(catalog.invalid_total, "Каталог LAB E40.invalid_total");
|
||||
exactString(catalog.access, "read-only", "Каталог LAB E40.access");
|
||||
if (!Array.isArray(catalog.items)) {
|
||||
throw new E40ProductGateContractError(
|
||||
"Каталог LAB E40.items: ожидался массив.",
|
||||
);
|
||||
}
|
||||
if (catalog.items.length > 1) {
|
||||
throw new E40ProductGateContractError(
|
||||
"Каталог LAB E40.items: нарушен limit=1.",
|
||||
);
|
||||
}
|
||||
return catalog.items.length ? parseResult(catalog.items[0]) : null;
|
||||
}
|
||||
|
||||
export async function fetchE40ProductGateResult({
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
}: {
|
||||
fetcher?: LaboratoryFetch;
|
||||
signal?: AbortSignal;
|
||||
} = {}): Promise<E40PerceptionProductGateResult | null> {
|
||||
const response = await fetcher(
|
||||
"/api/v1/laboratory/e40/results?limit=1",
|
||||
{
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
signal,
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new E40ProductGateContractError(
|
||||
`Каталог LAB E40 недоступен: HTTP ${response.status}.`,
|
||||
);
|
||||
}
|
||||
return parseCatalog(await response.json());
|
||||
}
|
||||
Reference in New Issue
Block a user