feat: 기능 추가 과정중

This commit is contained in:
donghyeon-ka
2026-07-30 15:58:20 +09:00
parent d3ef801fe6
commit 6c52cdb916
648 changed files with 126325 additions and 6680 deletions
+772
View File
@@ -0,0 +1,772 @@
import type { InstalledBoundaryMapper } from "./boundary-mapper.ts";
import type { RuntimeSchemaCodec } from "./schema-registry.ts";
export const BROWSER_RPC_CONTRACT_VERSION = 3 as const;
export const BROWSER_RPC_HARD_LIMITS = Object.freeze({
maxRequestMessageBytes: 8 * 1024 * 1024,
maxResponseMessageBytes: 8 * 1024 * 1024,
maxTotalResponseBytes: 64 * 1024 * 1024,
maxBufferedBytes: 16 * 1024 * 1024,
maxResponseMessages: 10_000,
maxDeadlineMs: 30 * 60_000,
maxAttempts: 4,
maxBackoffMs: 30_000,
maxRetryAfterMs: 60_000,
maxProfiles: 128,
maxOperations: 512,
});
export type BrowserRpcProtocol = "CONNECT_HTTP" | "GRPC_WEB";
export type BrowserRpcRuntimeKind =
| "CONNECT_WEB_FETCH"
| "OFFICIAL_GRPC_WEB_XHR"
| "CUSTOM_FETCH_FRAMED";
export type BrowserRpcClientApiKind =
| "PROMISE_UNARY"
| "ASYNC_ITERABLE"
| "CALLBACK_STREAM";
export type BrowserRpcKind = "UNARY" | "SERVER_STREAM";
export type BrowserRpcMessageEncoding = "PROTO" | "JSON";
export type BrowserRpcFraming =
| "CONNECT_BARE"
| "CONNECT_ENVELOPE"
| "GRPC_WEB_BINARY_ENVELOPE"
| "GRPC_WEB_BASE64_TEXT";
export type BrowserRpcRequestMethod = "POST" | "GET";
export type BrowserRpcSemantics = "QUERY" | "COMMAND" | "SERVER_STREAM";
export type BrowserRpcReplayPolicy =
| "SAFE"
| "IDEMPOTENT"
| "KEYED_COMMAND"
| "NON_REPLAYABLE";
export type BrowserRpcIdempotencyLevel =
| "NONE"
| "IDEMPOTENT"
| "NO_SIDE_EFFECTS";
export type BrowserRpcDataClassification =
| "PUBLIC"
| "INTERNAL"
| "CONFIDENTIAL";
export type BrowserRpcRetryOwner =
| "FRONTEND_ADAPTER"
| "EDGE_PROXY"
| "NONE";
export type BrowserRpcRawByteCeilingOwner =
| "EDGE_PROXY"
| "BOUNDED_TRANSPORT"
| "EDGE_AND_TRANSPORT";
export type BrowserRpcDeadlineDialect =
| "CONNECT_TIMEOUT_MS"
| "GRPC_TIMEOUT"
| "OFFICIAL_DEADLINE_METADATA";
export type BrowserRpcCancelDialect =
| "ABORT_SIGNAL"
| "CLIENT_READABLE_STREAM_CANCEL";
export type BrowserRpcTransportFailureCode =
| "NETWORK_UNREACHABLE"
| "CANCELED"
| "DEADLINE_EXCEEDED"
| "UNAUTHENTICATED"
| "PERMISSION_DENIED"
| "NOT_FOUND"
| "ALREADY_EXISTS"
| "ABORTED"
| "FAILED_PRECONDITION"
| "INVALID_ARGUMENT"
| "RESOURCE_EXHAUSTED"
| "UNAVAILABLE"
| "UNIMPLEMENTED"
| "INTERNAL"
| "DATA_LOSS"
| "PROTOCOL_MISMATCH"
| "MESSAGE_LIMIT";
export type BrowserRpcOperationV3 = Readonly<{
contractVersion: typeof BROWSER_RPC_CONTRACT_VERSION;
operationId: string;
owner: string;
protocol: BrowserRpcProtocol;
semantics: BrowserRpcSemantics;
replayPolicy: BrowserRpcReplayPolicy;
idempotencyKeyPolicy: "NONE" | "REQUIRED";
idempotencyLevel: BrowserRpcIdempotencyLevel;
dataClassification: BrowserRpcDataClassification;
runtimeProfileId: string;
providerId: string;
fullyQualifiedService: string;
method: string;
rpcKind: BrowserRpcKind;
requestMessageId: string;
responseMessageId: string;
descriptorArtifactId: string;
descriptorDigest: string;
requestSchemaId: string;
responseSchemaId: string;
requestEncoderId: string;
mapperId: string;
authProfileId: string;
csrfProfileId: string;
errorProfileId: string;
deadlineProfileId: string;
retryProfileId: string;
serverStateProfileId: string | null;
maxRequestMessageBytes: number;
maxResponseMessageBytes: number;
maxResponseMessages: number;
maxTotalResponseBytes: number;
maxBufferedBytes: number;
idleDeadlineMs: number | null;
totalDeadlineMs: number;
}>;
export type BrowserRpcProviderProfile = Readonly<{
runtimeProfileId: string;
providerId: string;
fixedBaseUrl: string;
runtimeId: string;
runtimeVersion: string;
runtimeDigest: string;
protocol: BrowserRpcProtocol;
runtimeKind: BrowserRpcRuntimeKind;
clientApiKind: BrowserRpcClientApiKind;
rpcKind: BrowserRpcKind;
messageEncoding: BrowserRpcMessageEncoding;
framing: BrowserRpcFraming;
requestMethod: BrowserRpcRequestMethod;
descriptorArtifactId: string;
descriptorDigest: string;
allowedProcedures: readonly string[];
authProfileId: string;
csrfProfileId: string;
corsProfileId: string;
errorProfileId: string;
deadlineProfileId: string;
retryProfileId: string;
retryOwner: BrowserRpcRetryOwner;
maxAttempts: number;
backoffMs: readonly number[];
retryableFailures: readonly BrowserRpcTransportFailureCode[];
maxRetryAfterMs: number;
deadlineDialect: BrowserRpcDeadlineDialect;
cancelDialect: BrowserRpcCancelDialect;
rawByteCeilingOwner: BrowserRpcRawByteCeilingOwner;
streamMessageCompression: "IDENTITY_ONLY";
}>;
export type BrowserRpcRequestEncoder = Readonly<{
encoderId: string;
operationId: string;
encode(value: unknown):
| Readonly<{ ok: true; value: unknown; encodedBytes: number }>
| Readonly<{ ok: false; code: string }>;
}>;
export type BrowserRpcRuntimeBindingIdentity = Readonly<{
runtimeProfileId: string;
providerId: string;
protocol: BrowserRpcProtocol;
rpcKind: BrowserRpcKind;
}>;
export type BrowserRpcContractBindings = Readonly<{
operations: Readonly<Record<string, BrowserRpcOperationV3>>;
profiles: Readonly<Record<string, BrowserRpcProviderProfile>>;
schemaCodecs: Readonly<Record<string, RuntimeSchemaCodec>>;
mappers: Readonly<Record<string, InstalledBoundaryMapper>>;
requestEncoders: Readonly<Record<string, BrowserRpcRequestEncoder>>;
runtimeBindings?: Readonly<Record<string, BrowserRpcRuntimeBindingIdentity>>;
}>;
const REGISTRY_ID = /^[A-Z][A-Z0-9_]{2,79}$/;
const ARTIFACT_ID = /^[A-Za-z][A-Za-z0-9_.:-]{2,159}$/;
const OWNER = /^[a-z][a-z0-9-]{2,159}$/;
const SERVICE =
/^(?:[a-z][a-z0-9_]*\.)+[A-Z][A-Za-z0-9_]{1,79}$/;
const METHOD = /^[A-Z][A-Za-z0-9_]{1,79}$/;
const VERSION = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,79}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const RETRYABLE_FAILURES = new Set<BrowserRpcTransportFailureCode>([
"NETWORK_UNREACHABLE",
"RESOURCE_EXHAUSTED",
"UNAVAILABLE",
]);
const PROTOCOLS = new Set<string>(["CONNECT_HTTP", "GRPC_WEB"]);
const RUNTIME_KINDS = new Set<string>([
"CONNECT_WEB_FETCH",
"OFFICIAL_GRPC_WEB_XHR",
"CUSTOM_FETCH_FRAMED",
]);
const CLIENT_API_KINDS = new Set<string>([
"PROMISE_UNARY",
"ASYNC_ITERABLE",
"CALLBACK_STREAM",
]);
const RPC_KINDS = new Set<string>(["UNARY", "SERVER_STREAM"]);
const MESSAGE_ENCODINGS = new Set<string>(["PROTO", "JSON"]);
const FRAMINGS = new Set<string>([
"CONNECT_BARE",
"CONNECT_ENVELOPE",
"GRPC_WEB_BINARY_ENVELOPE",
"GRPC_WEB_BASE64_TEXT",
]);
const REQUEST_METHODS = new Set<string>(["POST", "GET"]);
const SEMANTICS = new Set<string>([
"QUERY",
"COMMAND",
"SERVER_STREAM",
]);
const REPLAY_POLICIES = new Set<string>([
"SAFE",
"IDEMPOTENT",
"KEYED_COMMAND",
"NON_REPLAYABLE",
]);
const IDEMPOTENCY_KEY_POLICIES = new Set<string>(["NONE", "REQUIRED"]);
const IDEMPOTENCY_LEVELS = new Set<string>([
"NONE",
"IDEMPOTENT",
"NO_SIDE_EFFECTS",
]);
const DATA_CLASSIFICATIONS = new Set<string>([
"PUBLIC",
"INTERNAL",
"CONFIDENTIAL",
]);
const RETRY_OWNERS = new Set<string>([
"FRONTEND_ADAPTER",
"EDGE_PROXY",
"NONE",
]);
const DEADLINE_DIALECTS = new Set<string>([
"CONNECT_TIMEOUT_MS",
"GRPC_TIMEOUT",
"OFFICIAL_DEADLINE_METADATA",
]);
const CANCEL_DIALECTS = new Set<string>([
"ABORT_SIGNAL",
"CLIENT_READABLE_STREAM_CANCEL",
]);
const RAW_BYTE_CEILING_OWNERS = new Set<string>([
"EDGE_PROXY",
"BOUNDED_TRANSPORT",
"EDGE_AND_TRANSPORT",
]);
export function defineBrowserRpcOperation(
operation: BrowserRpcOperationV3,
): BrowserRpcOperationV3 {
validateOperation(operation);
return Object.freeze({ ...operation });
}
export function defineBrowserRpcProviderProfile(
profile: BrowserRpcProviderProfile,
): BrowserRpcProviderProfile {
validateProviderProfile(profile);
return Object.freeze({
...profile,
allowedProcedures: Object.freeze([...profile.allowedProcedures]),
backoffMs: Object.freeze([...profile.backoffMs]),
retryableFailures: Object.freeze([...profile.retryableFailures]),
});
}
export function defineBrowserRpcRequestEncoder(
encoder: BrowserRpcRequestEncoder,
): BrowserRpcRequestEncoder {
if (
!ARTIFACT_ID.test(encoder.encoderId) ||
!REGISTRY_ID.test(encoder.operationId) ||
typeof encoder.encode !== "function"
) {
throw new TypeError("Browser RPC request encoder is invalid.");
}
return Object.freeze({ ...encoder });
}
export function composeBrowserRpcOperationRegistry(
contributions: readonly Readonly<
Record<string, BrowserRpcOperationV3>
>[],
): Readonly<Record<string, BrowserRpcOperationV3>> {
return composeRegistry(
contributions,
(operation) => operation.operationId,
defineBrowserRpcOperation,
"operation",
BROWSER_RPC_HARD_LIMITS.maxOperations,
);
}
export function composeBrowserRpcProviderProfileRegistry(
contributions: readonly Readonly<
Record<string, BrowserRpcProviderProfile>
>[],
): Readonly<Record<string, BrowserRpcProviderProfile>> {
return composeRegistry(
contributions,
(profile) => profile.runtimeProfileId,
defineBrowserRpcProviderProfile,
"provider profile",
BROWSER_RPC_HARD_LIMITS.maxProfiles,
);
}
export function composeBrowserRpcRequestEncoderRegistry(
contributions: readonly Readonly<
Record<string, BrowserRpcRequestEncoder>
>[],
): Readonly<Record<string, BrowserRpcRequestEncoder>> {
return composeRegistry(
contributions,
(encoder) => encoder.encoderId,
defineBrowserRpcRequestEncoder,
"request encoder",
BROWSER_RPC_HARD_LIMITS.maxOperations,
);
}
export function validateBrowserRpcContractBindings(
bindings: BrowserRpcContractBindings,
): true {
for (const [profileId, profile] of Object.entries(bindings.profiles)) {
if (profileId !== profile.runtimeProfileId) {
throw new TypeError(
`Browser RPC provider profile registry is invalid: ${profileId}`,
);
}
validateProviderProfile(profile);
}
for (const [schemaId, schema] of Object.entries(bindings.schemaCodecs)) {
if (schemaId !== schema.schemaId || typeof schema.parse !== "function") {
throw new TypeError(
`Browser RPC schema registry is invalid: ${schemaId}`,
);
}
}
for (const [mapperId, mapper] of Object.entries(bindings.mappers)) {
if (
mapperId !== mapper.mapperId ||
typeof mapper.map !== "function" ||
!Number.isSafeInteger(mapper.mapperVersion) ||
mapper.mapperVersion < 1
) {
throw new TypeError(
`Browser RPC mapper registry is invalid: ${mapperId}`,
);
}
}
for (const [encoderId, encoder] of Object.entries(
bindings.requestEncoders,
)) {
if (encoderId !== encoder.encoderId) {
throw new TypeError(
`Browser RPC request encoder registry is invalid: ${encoderId}`,
);
}
defineBrowserRpcRequestEncoder(encoder);
}
for (const [profileId, runtime] of Object.entries(
bindings.runtimeBindings ?? {},
)) {
if (
profileId !== runtime.runtimeProfileId ||
!REGISTRY_ID.test(runtime.runtimeProfileId) ||
!REGISTRY_ID.test(runtime.providerId) ||
!PROTOCOLS.has(runtime.protocol) ||
!RPC_KINDS.has(runtime.rpcKind)
) {
throw new TypeError(
`Browser RPC runtime registry is invalid: ${profileId}`,
);
}
}
for (const [operationId, operation] of Object.entries(
bindings.operations,
)) {
if (operationId !== operation.operationId) {
throw new TypeError(
`Browser RPC operation registry is invalid: ${operationId}`,
);
}
validateOperation(operation);
const profile = bindings.profiles[operation.runtimeProfileId];
const requestSchema = bindings.schemaCodecs[operation.requestSchemaId];
const responseSchema = bindings.schemaCodecs[operation.responseSchemaId];
const mapper = bindings.mappers[operation.mapperId];
const encoder = bindings.requestEncoders[operation.requestEncoderId];
const runtime = bindings.runtimeBindings?.[operation.runtimeProfileId];
const procedure = `${operation.fullyQualifiedService}/${operation.method}`;
if (
!profile ||
profile.providerId !== operation.providerId ||
profile.protocol !== operation.protocol ||
profile.rpcKind !== operation.rpcKind ||
profile.descriptorArtifactId !== operation.descriptorArtifactId ||
profile.descriptorDigest !== operation.descriptorDigest ||
profile.authProfileId !== operation.authProfileId ||
profile.csrfProfileId !== operation.csrfProfileId ||
profile.errorProfileId !== operation.errorProfileId ||
profile.deadlineProfileId !== operation.deadlineProfileId ||
profile.retryProfileId !== operation.retryProfileId ||
!profile.allowedProcedures.includes(procedure)
) {
throw new TypeError(
`Browser RPC provider binding is invalid: ${operation.operationId}`,
);
}
if (
requestSchema?.schemaId !== operation.requestSchemaId ||
responseSchema?.schemaId !== operation.responseSchemaId ||
mapper?.mapperId !== operation.mapperId ||
mapper.inputSchemaId !== operation.responseSchemaId ||
mapper.maxOutputItems < 1 ||
encoder?.encoderId !== operation.requestEncoderId ||
encoder.operationId !== operation.operationId
) {
throw new TypeError(
`Browser RPC schema/mapper binding is invalid: ${operation.operationId}`,
);
}
if (
runtime &&
(runtime.runtimeProfileId !== operation.runtimeProfileId ||
runtime.providerId !== operation.providerId ||
runtime.protocol !== operation.protocol ||
runtime.rpcKind !== operation.rpcKind)
) {
throw new TypeError(
`Browser RPC runtime binding is invalid: ${operation.operationId}`,
);
}
if (
profile.requestMethod === "GET" &&
(operation.protocol !== "CONNECT_HTTP" ||
operation.rpcKind !== "UNARY" ||
operation.semantics !== "QUERY" ||
operation.replayPolicy !== "SAFE" ||
operation.idempotencyLevel !== "NO_SIDE_EFFECTS" ||
operation.dataClassification !== "PUBLIC" ||
operation.authProfileId !== "ANONYMOUS" ||
operation.csrfProfileId !== "NONE")
) {
throw new TypeError(
`Browser RPC GET binding is invalid: ${operation.operationId}`,
);
}
if (
profile.retryOwner === "FRONTEND_ADAPTER" &&
!isFrontendReplayAllowed(operation)
) {
throw new TypeError(
`Browser RPC retry binding is invalid: ${operation.operationId}`,
);
}
}
return true;
}
function validateOperation(operation: BrowserRpcOperationV3): void {
if (
operation.contractVersion !== BROWSER_RPC_CONTRACT_VERSION ||
!REGISTRY_ID.test(operation.operationId) ||
!OWNER.test(operation.owner) ||
!PROTOCOLS.has(operation.protocol) ||
!SEMANTICS.has(operation.semantics) ||
!REPLAY_POLICIES.has(operation.replayPolicy) ||
!IDEMPOTENCY_KEY_POLICIES.has(operation.idempotencyKeyPolicy) ||
!IDEMPOTENCY_LEVELS.has(operation.idempotencyLevel) ||
!DATA_CLASSIFICATIONS.has(operation.dataClassification) ||
!REGISTRY_ID.test(operation.runtimeProfileId) ||
!REGISTRY_ID.test(operation.providerId) ||
!SERVICE.test(operation.fullyQualifiedService) ||
!METHOD.test(operation.method) ||
!RPC_KINDS.has(operation.rpcKind) ||
!ARTIFACT_ID.test(operation.requestMessageId) ||
!ARTIFACT_ID.test(operation.responseMessageId) ||
!ARTIFACT_ID.test(operation.descriptorArtifactId) ||
!SHA256.test(operation.descriptorDigest) ||
!ARTIFACT_ID.test(operation.requestSchemaId) ||
!ARTIFACT_ID.test(operation.responseSchemaId) ||
!ARTIFACT_ID.test(operation.requestEncoderId) ||
!ARTIFACT_ID.test(operation.mapperId) ||
!REGISTRY_ID.test(operation.authProfileId) ||
!REGISTRY_ID.test(operation.csrfProfileId) ||
!REGISTRY_ID.test(operation.errorProfileId) ||
!REGISTRY_ID.test(operation.deadlineProfileId) ||
!REGISTRY_ID.test(operation.retryProfileId) ||
(operation.serverStateProfileId !== null &&
!ARTIFACT_ID.test(operation.serverStateProfileId)) ||
!positiveIntegerWithin(
operation.maxRequestMessageBytes,
BROWSER_RPC_HARD_LIMITS.maxRequestMessageBytes,
) ||
!positiveIntegerWithin(
operation.maxResponseMessageBytes,
BROWSER_RPC_HARD_LIMITS.maxResponseMessageBytes,
) ||
!positiveIntegerWithin(
operation.maxResponseMessages,
BROWSER_RPC_HARD_LIMITS.maxResponseMessages,
) ||
!positiveIntegerWithin(
operation.maxTotalResponseBytes,
BROWSER_RPC_HARD_LIMITS.maxTotalResponseBytes,
) ||
!positiveIntegerWithin(
operation.maxBufferedBytes,
BROWSER_RPC_HARD_LIMITS.maxBufferedBytes,
) ||
!positiveIntegerWithin(
operation.totalDeadlineMs,
BROWSER_RPC_HARD_LIMITS.maxDeadlineMs,
) ||
operation.maxTotalResponseBytes < operation.maxResponseMessageBytes ||
operation.maxBufferedBytes < operation.maxResponseMessageBytes
) {
throw new TypeError(
`Invalid Browser RPC operation: ${operation.operationId}`,
);
}
const unary = operation.rpcKind === "UNARY";
if (
(unary &&
(operation.semantics === "SERVER_STREAM" ||
operation.maxResponseMessages !== 1 ||
operation.idleDeadlineMs !== null)) ||
(!unary &&
(operation.semantics !== "SERVER_STREAM" ||
!positiveIntegerWithin(
operation.idleDeadlineMs,
operation.totalDeadlineMs,
))) ||
(operation.semantics === "QUERY" &&
!["SAFE", "IDEMPOTENT"].includes(operation.replayPolicy)) ||
(operation.semantics === "COMMAND" &&
["SAFE", "IDEMPOTENT"].includes(operation.replayPolicy)) ||
(operation.replayPolicy === "KEYED_COMMAND" &&
operation.idempotencyKeyPolicy !== "REQUIRED") ||
(operation.replayPolicy !== "KEYED_COMMAND" &&
operation.idempotencyKeyPolicy !== "NONE") ||
(operation.idempotencyLevel === "NO_SIDE_EFFECTS" &&
operation.semantics !== "QUERY")
) {
throw new TypeError(
`Incoherent Browser RPC operation: ${operation.operationId}`,
);
}
}
function validateProviderProfile(profile: BrowserRpcProviderProfile): void {
let endpoint: URL;
try {
endpoint = new URL(profile.fixedBaseUrl);
} catch {
throw new TypeError("Browser RPC provider profile is invalid.");
}
const localHttp =
endpoint.protocol === "http:" &&
["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname);
if (
!REGISTRY_ID.test(profile.runtimeProfileId) ||
!REGISTRY_ID.test(profile.providerId) ||
!ARTIFACT_ID.test(profile.runtimeId) ||
!VERSION.test(profile.runtimeVersion) ||
!SHA256.test(profile.runtimeDigest) ||
!PROTOCOLS.has(profile.protocol) ||
!RUNTIME_KINDS.has(profile.runtimeKind) ||
!CLIENT_API_KINDS.has(profile.clientApiKind) ||
!RPC_KINDS.has(profile.rpcKind) ||
!MESSAGE_ENCODINGS.has(profile.messageEncoding) ||
!FRAMINGS.has(profile.framing) ||
!REQUEST_METHODS.has(profile.requestMethod) ||
!ARTIFACT_ID.test(profile.descriptorArtifactId) ||
!SHA256.test(profile.descriptorDigest) ||
!REGISTRY_ID.test(profile.authProfileId) ||
!REGISTRY_ID.test(profile.csrfProfileId) ||
!REGISTRY_ID.test(profile.corsProfileId) ||
!REGISTRY_ID.test(profile.errorProfileId) ||
!REGISTRY_ID.test(profile.deadlineProfileId) ||
!REGISTRY_ID.test(profile.retryProfileId) ||
!RETRY_OWNERS.has(profile.retryOwner) ||
!DEADLINE_DIALECTS.has(profile.deadlineDialect) ||
!CANCEL_DIALECTS.has(profile.cancelDialect) ||
!RAW_BYTE_CEILING_OWNERS.has(profile.rawByteCeilingOwner) ||
profile.streamMessageCompression !== "IDENTITY_ONLY" ||
(endpoint.protocol !== "https:" && !localHttp) ||
endpoint.username ||
endpoint.password ||
endpoint.search ||
endpoint.hash ||
profile.allowedProcedures.length === 0 ||
profile.allowedProcedures.length > BROWSER_RPC_HARD_LIMITS.maxOperations ||
new Set(profile.allowedProcedures).size !==
profile.allowedProcedures.length ||
profile.allowedProcedures.some((procedure) => {
const separator = procedure.lastIndexOf("/");
return (
separator < 1 ||
!SERVICE.test(procedure.slice(0, separator)) ||
!METHOD.test(procedure.slice(separator + 1))
);
}) ||
!positiveIntegerWithin(
profile.maxAttempts,
BROWSER_RPC_HARD_LIMITS.maxAttempts,
) ||
!nonNegativeIntegerWithin(
profile.maxRetryAfterMs,
BROWSER_RPC_HARD_LIMITS.maxRetryAfterMs,
) ||
new Set(profile.retryableFailures).size !==
profile.retryableFailures.length ||
profile.retryableFailures.some(
(failure) => !RETRYABLE_FAILURES.has(failure),
) ||
profile.backoffMs.some(
(delay) =>
!nonNegativeIntegerWithin(
delay,
BROWSER_RPC_HARD_LIMITS.maxBackoffMs,
),
) ||
(profile.retryOwner === "FRONTEND_ADAPTER"
? profile.maxAttempts < 2 ||
profile.backoffMs.length !== profile.maxAttempts - 1 ||
profile.retryableFailures.length === 0
: profile.maxAttempts !== 1 ||
profile.backoffMs.length !== 0 ||
profile.retryableFailures.length !== 0) ||
(profile.rpcKind === "SERVER_STREAM" &&
profile.retryOwner !== "NONE") ||
!runtimeTupleIsValid(profile)
) {
throw new TypeError("Browser RPC provider profile is invalid.");
}
}
function runtimeTupleIsValid(profile: BrowserRpcProviderProfile): boolean {
if (profile.protocol === "CONNECT_HTTP") {
if (
profile.runtimeKind !== "CONNECT_WEB_FETCH" ||
profile.deadlineDialect !== "CONNECT_TIMEOUT_MS" ||
profile.cancelDialect !== "ABORT_SIGNAL"
) {
return false;
}
if (profile.rpcKind === "UNARY") {
return (
profile.clientApiKind === "PROMISE_UNARY" &&
profile.framing === "CONNECT_BARE"
);
}
return (
profile.requestMethod === "POST" &&
profile.clientApiKind === "ASYNC_ITERABLE" &&
profile.framing === "CONNECT_ENVELOPE"
);
}
if (profile.requestMethod !== "POST") return false;
if (profile.runtimeKind === "OFFICIAL_GRPC_WEB_XHR") {
if (
profile.messageEncoding !== "PROTO" ||
profile.deadlineDialect !== "OFFICIAL_DEADLINE_METADATA"
) {
return false;
}
if (profile.rpcKind === "UNARY") {
return (
["PROMISE_UNARY", "CALLBACK_STREAM"].includes(
profile.clientApiKind,
) &&
["GRPC_WEB_BINARY_ENVELOPE", "GRPC_WEB_BASE64_TEXT"].includes(
profile.framing,
) &&
(profile.clientApiKind === "CALLBACK_STREAM"
? profile.cancelDialect === "CLIENT_READABLE_STREAM_CANCEL"
: profile.cancelDialect === "ABORT_SIGNAL")
);
}
return (
profile.clientApiKind === "CALLBACK_STREAM" &&
profile.framing === "GRPC_WEB_BASE64_TEXT" &&
profile.cancelDialect === "CLIENT_READABLE_STREAM_CANCEL"
);
}
if (profile.runtimeKind === "CONNECT_WEB_FETCH") {
return (
profile.deadlineDialect === "GRPC_TIMEOUT" &&
profile.cancelDialect === "ABORT_SIGNAL" &&
profile.framing === "GRPC_WEB_BINARY_ENVELOPE" &&
(profile.rpcKind === "UNARY"
? profile.clientApiKind === "PROMISE_UNARY"
: profile.clientApiKind === "ASYNC_ITERABLE")
);
}
return (
profile.runtimeKind === "CUSTOM_FETCH_FRAMED" &&
profile.deadlineDialect === "GRPC_TIMEOUT" &&
profile.cancelDialect === "ABORT_SIGNAL" &&
(profile.rpcKind === "UNARY"
? profile.clientApiKind === "PROMISE_UNARY"
: profile.clientApiKind === "ASYNC_ITERABLE")
);
}
function isFrontendReplayAllowed(
operation: BrowserRpcOperationV3,
): boolean {
return (
["SAFE", "IDEMPOTENT"].includes(operation.replayPolicy) ||
(operation.replayPolicy === "KEYED_COMMAND" &&
operation.idempotencyKeyPolicy === "REQUIRED")
);
}
function composeRegistry<Value>(
contributions: readonly Readonly<Record<string, Value>>[],
identity: (value: Value) => string,
define: (value: Value) => Value,
label: string,
maximumRows: number,
): Readonly<Record<string, Value>> {
const result: Record<string, Value> = Object.create(null);
let rows = 0;
for (const contribution of contributions) {
for (const [registryId, value] of Object.entries(contribution)) {
rows += 1;
if (
rows > maximumRows ||
registryId !== identity(value) ||
Object.hasOwn(result, registryId)
) {
throw new TypeError(
`Invalid or duplicate Browser RPC ${label}: ${registryId}`,
);
}
result[registryId] = define(value);
}
}
return Object.freeze(result);
}
function positiveIntegerWithin(
value: number | null,
maximum: number,
): value is number {
return Number.isSafeInteger(value) && value !== null && value > 0 && value <= maximum;
}
function nonNegativeIntegerWithin(
value: number,
maximum: number,
): boolean {
return Number.isSafeInteger(value) && value >= 0 && value <= maximum;
}