blob: 8329e430459749bcc32e6a99e6c52902c0edf5e8 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
RUNTIME_HOST_COMPATIBILITY_EPOCH,
RUNTIME_HOST_PROTOCOL_VERSION,
type HostIncompatible,
} from '../protocol/index.js';
import { RuntimeHostPermanentReconnectError } from './reconnect-lifecycle.js';
export const RUNTIME_HOST_REMOTE_INCOMPATIBLE_CODE = 'RUNTIME_HOST_REMOTE_INCOMPATIBLE' as const;
export interface RuntimeHostRemoteCompatibilityDetails {
readonly profileId: string;
readonly client: {
readonly compatibilityEpoch: number;
readonly protocolMin: number;
readonly protocolMax: number;
readonly compositionId: string;
};
readonly host: {
readonly compatibilityEpoch: number;
readonly protocolMin: number;
readonly protocolMax: number;
readonly compositionId: string;
readonly compositionRevision: string;
};
}
export class RuntimeHostRemoteCompatibilityError extends RuntimeHostPermanentReconnectError {
readonly name = 'RuntimeHostRemoteCompatibilityError';
readonly code = RUNTIME_HOST_REMOTE_INCOMPATIBLE_CODE;
readonly details: RuntimeHostRemoteCompatibilityDetails;
readonly hostEpoch: string;
constructor(profileId: string, handshake: HostIncompatible) {
const details = Object.freeze({
profileId,
client: Object.freeze({
compatibilityEpoch: RUNTIME_HOST_COMPATIBILITY_EPOCH,
protocolMin: RUNTIME_HOST_PROTOCOL_VERSION,
protocolMax: RUNTIME_HOST_PROTOCOL_VERSION,
compositionId: INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
}),
host: Object.freeze({
compatibilityEpoch: handshake.compatibilityEpoch,
protocolMin: handshake.protocolMin,
protocolMax: handshake.protocolMax,
compositionId: handshake.compositionId,
compositionRevision: handshake.compositionRevision,
}),
});
super(formatRuntimeHostRemoteCompatibilityMessage(details));
this.details = details;
this.hostEpoch = handshake.hostEpoch;
}
}
function formatRuntimeHostRemoteCompatibilityMessage(
details: RuntimeHostRemoteCompatibilityDetails,
): string {
const message = [
`${RUNTIME_HOST_REMOTE_INCOMPATIBLE_CODE}: Runtime Host profile ${details.profileId} is incompatible`,
`Client compatibility epoch ${details.client.compatibilityEpoch}`,
`Host compatibility epoch ${details.host.compatibilityEpoch}`,
];
if (!protocolRangesOverlap(details.client, details.host)) {
message.push(
`Client protocol range ${details.client.protocolMin}-${details.client.protocolMax}`,
`Host protocol range ${details.host.protocolMin}-${details.host.protocolMax}`,
);
}
if (details.client.compositionId !== details.host.compositionId) {
message.push(
`Client composition id ${details.client.compositionId}`,
`Host composition id ${formatRuntimeHostDiagnosticValue(details.host.compositionId)}`,
`Host composition revision ${formatRuntimeHostDiagnosticValue(details.host.compositionRevision)}`,
);
}
message.push(
'Use compatible Client and Host builds, restart the remote Runtime Host service after updating the Host, then retry',
);
return message.join('. ');
}
function formatRuntimeHostDiagnosticValue(value: string): string {
return value.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, '\uFFFD');
}
function protocolRangesOverlap(
client: RuntimeHostRemoteCompatibilityDetails['client'],
host: RuntimeHostRemoteCompatibilityDetails['host'],
): boolean {
return client.protocolMin <= host.protocolMax && host.protocolMin <= client.protocolMax;
}