初始化项目
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||
*/
|
||||
export const HAR_VERSION = '1.0.0';
|
||||
export const BUILD_MODE_NAME = 'debug';
|
||||
export const DEBUG = true;
|
||||
export const TARGET_NAME = 'default';
|
||||
|
||||
/**
|
||||
* BuildProfile Class is used only for compatibility purposes.
|
||||
*/
|
||||
export default class BuildProfile {
|
||||
static readonly HAR_VERSION = HAR_VERSION;
|
||||
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||
static readonly DEBUG = DEBUG;
|
||||
static readonly TARGET_NAME = TARGET_NAME;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @kuge/engine 的唯一公开入口。
|
||||
*
|
||||
* 约定(见 docs/adr/ADR-001):HAR 模块只通过本文件暴露能力,
|
||||
* 外部模块禁止直接 import src/main/ets 下的实现文件。
|
||||
*/
|
||||
export {
|
||||
ConnectionConfig,
|
||||
ConnectionError,
|
||||
ConnectionState,
|
||||
Driver,
|
||||
DriverFactory,
|
||||
EngineKind,
|
||||
engineLabel,
|
||||
stateLabel
|
||||
} from './src/main/ets/connection/types';
|
||||
export { Connection, ConnectionManager } from './src/main/ets/connection/ConnectionManager';
|
||||
export { Frame, FrameCodec } from './src/main/ets/protocol/FrameCodec';
|
||||
export { MockDriver, MockDriverFactory } from './src/main/ets/driver/MockDriver';
|
||||
export { EnginePlugin, PluginMeta } from './src/main/ets/plugin/EnginePlugin';
|
||||
export { EngineRegistry, ENGINE_API_VERSION } from './src/main/ets/plugin/EngineRegistry';
|
||||
export { SemVerUtil } from './src/main/ets/plugin/SemVerUtil';
|
||||
export { ConnectionTester, TestResult, TestStage, TestErrorCode } from './src/main/ets/connection/ConnectionTester';
|
||||
export { AppError, ErrorCategory, ErrorSeverity } from './src/main/ets/error/AppError';
|
||||
export { ErrorMapper } from './src/main/ets/error/ErrorMapper';
|
||||
export { ReconnectPolicy, ReconnectOptions } from './src/main/ets/connection/ReconnectPolicy';
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {},
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
|
||||
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@kuge/engine",
|
||||
"version": "1.0.0",
|
||||
"description": "Kuge engine layer: connection abstraction, protocol framing, drivers",
|
||||
"main": "Index.ets",
|
||||
"author": "Kuge Team",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 连接管理内核。
|
||||
*
|
||||
* 职责:按引擎选择驱动、维护连接状态机、提供超时与关闭语义。
|
||||
* 连接池策略(按引擎配额、复用、空闲回收)在 1.4.19 会话恢复与 1.4.1 后续迭代中补,
|
||||
* 这里只保留"可注册、可创建、可关闭"的最小闭环。
|
||||
*/
|
||||
|
||||
import { ConnectionConfig, ConnectionError, ConnectionState, Driver, DriverFactory, EngineKind } from './types';
|
||||
|
||||
/** 单条连接句柄。 */
|
||||
export class Connection {
|
||||
readonly config: ConnectionConfig;
|
||||
private readonly driver: Driver;
|
||||
private state: ConnectionState = ConnectionState.IDLE;
|
||||
|
||||
constructor(config: ConnectionConfig, driver: Driver) {
|
||||
this.config = config;
|
||||
this.driver = driver;
|
||||
}
|
||||
|
||||
getState(): ConnectionState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (this.state === ConnectionState.CONNECTED) {
|
||||
return;
|
||||
}
|
||||
this.state = ConnectionState.CONNECTING;
|
||||
try {
|
||||
await this.withTimeout(this.driver.connect(this.config));
|
||||
this.state = ConnectionState.CONNECTED;
|
||||
} catch (err) {
|
||||
this.state = ConnectionState.FAILED;
|
||||
const message: string = err instanceof Error ? err.message : String(err);
|
||||
throw new ConnectionError(message);
|
||||
}
|
||||
}
|
||||
|
||||
async ping(): Promise<void> {
|
||||
if (this.state !== ConnectionState.CONNECTED) {
|
||||
throw new ConnectionError('connection is not ready, state=' + this.state);
|
||||
}
|
||||
await this.driver.ping();
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.state === ConnectionState.CLOSED || this.state === ConnectionState.IDLE) {
|
||||
this.state = ConnectionState.CLOSED;
|
||||
return;
|
||||
}
|
||||
this.state = ConnectionState.DISCONNECTING;
|
||||
try {
|
||||
await this.driver.disconnect();
|
||||
} catch (err) {
|
||||
// 关闭失败不阻断清理,状态仍置为 CLOSED。
|
||||
}
|
||||
this.state = ConnectionState.CLOSED;
|
||||
}
|
||||
|
||||
private withTimeout(task: Promise<void>): Promise<void> {
|
||||
const ms: number = this.config.connectTimeoutMs;
|
||||
const timeout = new Promise<void>((_resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new ConnectionError('connect timeout after ' + ms + 'ms'));
|
||||
}, ms);
|
||||
});
|
||||
return Promise.race([task, timeout]);
|
||||
}
|
||||
}
|
||||
|
||||
/** 连接管理器。 */
|
||||
export class ConnectionManager {
|
||||
private factories: Map<EngineKind, DriverFactory> = new Map<EngineKind, DriverFactory>();
|
||||
private connections: Map<string, Connection> = new Map<string, Connection>();
|
||||
private order: string[] = [];
|
||||
|
||||
/** 注册某引擎的驱动工厂,后注册覆盖先注册。 */
|
||||
register(kind: EngineKind, factory: DriverFactory): void {
|
||||
this.factories.set(kind, factory);
|
||||
}
|
||||
|
||||
isSupported(kind: EngineKind): boolean {
|
||||
return this.factories.has(kind);
|
||||
}
|
||||
|
||||
/** 创建连接;id 重复或引擎未注册时抛出 ConnectionError。 */
|
||||
create(config: ConnectionConfig): Connection {
|
||||
if (this.connections.has(config.id)) {
|
||||
throw new ConnectionError('duplicate connection id: ' + config.id);
|
||||
}
|
||||
const factory = this.factories.get(config.engine);
|
||||
if (factory === undefined) {
|
||||
throw new ConnectionError('no driver registered for engine: ' + config.engine);
|
||||
}
|
||||
const conn = new Connection(config, factory.create());
|
||||
this.connections.set(config.id, conn);
|
||||
this.order.push(config.id);
|
||||
return conn;
|
||||
}
|
||||
|
||||
get(id: string): Connection | undefined {
|
||||
return this.connections.get(id);
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this.order.length;
|
||||
}
|
||||
|
||||
/** 关闭全部连接并清空注册表。 */
|
||||
async closeAll(): Promise<void> {
|
||||
for (let i = 0; i < this.order.length; i++) {
|
||||
const conn = this.connections.get(this.order[i]);
|
||||
if (conn !== undefined) {
|
||||
await conn.close();
|
||||
}
|
||||
}
|
||||
this.connections.clear();
|
||||
this.order = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 连接测试服务(1.4.2)。
|
||||
*
|
||||
* 目标:把「解析凭据 → 建连 → ping」拆成可诊断的阶段,返回稳定的 TestResult,
|
||||
* 供连接对话框与诊断面板展示(原因 + 建议)。
|
||||
*
|
||||
* 设计取舍:凭据由调用方解析后传入(secret),引擎层不直接依赖 core 的凭据实现,
|
||||
* 避免 ArkTS 名义类型带来的跨模块适配样板。
|
||||
*/
|
||||
import { ConnectionConfig, ConnectionError, Driver, DriverFactory, EngineKind } from './types';
|
||||
import { Connection, ConnectionManager } from './ConnectionManager';
|
||||
|
||||
/** 失败阶段。 */
|
||||
export enum TestStage {
|
||||
RESOLVE = 0,
|
||||
CONNECT = 1,
|
||||
PING = 2,
|
||||
DONE = 3
|
||||
}
|
||||
|
||||
/** 测试结果。 */
|
||||
export interface TestResult {
|
||||
ok: boolean;
|
||||
stage: TestStage;
|
||||
errorCode: string;
|
||||
message: string;
|
||||
elapsedMs: number;
|
||||
}
|
||||
|
||||
/** 分阶段诊断的 code 约定(对上层稳定,可映射多语言)。 */
|
||||
export class TestErrorCode {
|
||||
static readonly OK: string = 'KUGE-CONN-0000';
|
||||
static readonly CREDENTIAL_MISSING: string = 'KUGE-CONN-0001';
|
||||
static readonly CONNECT_FAILED: string = 'KUGE-CONN-0002';
|
||||
static readonly PING_FAILED: string = 'KUGE-CONN-0003';
|
||||
}
|
||||
|
||||
export class ConnectionTester {
|
||||
private readonly manager: ConnectionManager;
|
||||
|
||||
constructor(manager: ConnectionManager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行连接测试。
|
||||
* @param config 连接配置
|
||||
* @param secret 已解析的凭据;config.credentialRef 非空时必须提供
|
||||
*/
|
||||
async test(config: ConnectionConfig, secret: string | undefined): Promise<TestResult> {
|
||||
const started: number = Date.now();
|
||||
if (config.credentialRef.length > 0 && (secret === undefined || secret.length === 0)) {
|
||||
return this.fail(TestStage.RESOLVE, TestErrorCode.CREDENTIAL_MISSING, '凭据未找到,请重新输入密码', started);
|
||||
}
|
||||
let conn: Connection;
|
||||
try {
|
||||
conn = this.manager.create(config);
|
||||
await conn.connect();
|
||||
} catch (err) {
|
||||
const msg: string = err instanceof Error ? err.message : String(err);
|
||||
return this.fail(TestStage.CONNECT, TestErrorCode.CONNECT_FAILED, msg, started);
|
||||
}
|
||||
try {
|
||||
await conn.ping();
|
||||
} catch (err) {
|
||||
const msg: string = err instanceof Error ? err.message : String(err);
|
||||
await conn.close();
|
||||
return this.fail(TestStage.PING, TestErrorCode.PING_FAILED, msg, started);
|
||||
}
|
||||
await conn.close();
|
||||
return {
|
||||
ok: true,
|
||||
stage: TestStage.DONE,
|
||||
errorCode: TestErrorCode.OK,
|
||||
message: '连接成功',
|
||||
elapsedMs: Date.now() - started
|
||||
};
|
||||
}
|
||||
|
||||
private fail(stage: TestStage, code: string, message: string, started: number): TestResult {
|
||||
return {
|
||||
ok: false,
|
||||
stage: stage,
|
||||
errorCode: code,
|
||||
message: message,
|
||||
elapsedMs: Date.now() - started
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 断线重连策略(1.4.19)。
|
||||
*
|
||||
* 纯策略逻辑:决定「是否重试」与「等多久」。真实重连动作由 Connection 的调用方执行,
|
||||
* 定时器的接入在平台层完成,便于单元测试与后续加入抖动(jitter)。
|
||||
*
|
||||
* 规则:
|
||||
* - 指数退避:delay = min(base * multiplier^attempt, maxDelay);
|
||||
* - 认证/协议/资源限制类错误不重试(重试也不会成功);
|
||||
* - 达到最大次数后不再重试。
|
||||
*/
|
||||
import { ErrorCategory } from '../error/AppError';
|
||||
|
||||
export interface ReconnectOptions {
|
||||
/** 最大尝试次数(不含首次连接)。 */
|
||||
maxAttempts: number;
|
||||
/** 首次退避,毫秒。 */
|
||||
baseDelayMs: number;
|
||||
/** 退避倍数。 */
|
||||
multiplier: number;
|
||||
/** 退避上限,毫秒。 */
|
||||
maxDelayMs: number;
|
||||
}
|
||||
|
||||
export class ReconnectPolicy {
|
||||
static defaults(): ReconnectOptions {
|
||||
return { maxAttempts: 3, baseDelayMs: 500, multiplier: 2, maxDelayMs: 5000 };
|
||||
}
|
||||
|
||||
/** 第 attempt 次重试(0 起)的等待时间。 */
|
||||
static delayFor(attempt: number, options: ReconnectOptions): number {
|
||||
if (attempt < 0) {
|
||||
return 0;
|
||||
}
|
||||
let delay: number = options.baseDelayMs;
|
||||
for (let i = 0; i < attempt; i++) {
|
||||
delay = delay * options.multiplier;
|
||||
if (delay >= options.maxDelayMs) {
|
||||
return options.maxDelayMs;
|
||||
}
|
||||
}
|
||||
return delay > options.maxDelayMs ? options.maxDelayMs : delay;
|
||||
}
|
||||
|
||||
/** 是否应继续重试。 */
|
||||
static shouldRetry(attempt: number, options: ReconnectOptions, category: ErrorCategory): boolean {
|
||||
if (attempt >= options.maxAttempts) {
|
||||
return false;
|
||||
}
|
||||
if (category === ErrorCategory.AUTH || category === ErrorCategory.PROTOCOL || category === ErrorCategory.LIMIT) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 连接层公共类型。
|
||||
*
|
||||
* 设计约束(对应 WBS 1.4.1 / ADR-001):
|
||||
* 1. 凭据不进入配置结构,只保存 credentialRef,实际解密由 1.3.8 保险箱负责;
|
||||
* 2. 引擎以枚举收敛,新增引擎只在末尾追加,保持数值稳定;
|
||||
* 3. 驱动只暴露 connect / disconnect / ping 三个生命周期动作,查询与元数据
|
||||
* 能力在 1.4.x 后续工作包中按接口扩展,避免一开始就把契约定死。
|
||||
*/
|
||||
|
||||
/** 支持的数据库引擎(10 种,不含 ODBC / Access / DBase)。 */
|
||||
export enum EngineKind {
|
||||
SQLITE = 0,
|
||||
MYSQL = 1,
|
||||
MARIADB = 2,
|
||||
POSTGRESQL = 3,
|
||||
SQLSERVER = 4,
|
||||
ORACLE = 5,
|
||||
MONGODB = 6,
|
||||
REDIS = 7,
|
||||
OCEANBASE = 8,
|
||||
TIDB = 9
|
||||
}
|
||||
|
||||
/** 连接生命周期状态。 */
|
||||
export enum ConnectionState {
|
||||
IDLE = 0,
|
||||
CONNECTING = 1,
|
||||
CONNECTED = 2,
|
||||
DISCONNECTING = 3,
|
||||
CLOSED = 4,
|
||||
FAILED = 5
|
||||
}
|
||||
|
||||
/** 连接配置。 */
|
||||
export interface ConnectionConfig {
|
||||
/** 连接唯一标识,由连接管理器保证不重复。 */
|
||||
id: string;
|
||||
/** 用户可见名称。 */
|
||||
name: string;
|
||||
engine: EngineKind;
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
user: string;
|
||||
/** 凭据引用,不存明文。 */
|
||||
credentialRef: string;
|
||||
/** 建连超时,毫秒。 */
|
||||
connectTimeoutMs: number;
|
||||
}
|
||||
|
||||
/** 引擎驱动契约。 */
|
||||
export interface Driver {
|
||||
readonly kind: EngineKind;
|
||||
connect(config: ConnectionConfig): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
ping(): Promise<void>;
|
||||
}
|
||||
|
||||
/** 驱动工厂。 */
|
||||
export interface DriverFactory {
|
||||
create(): Driver;
|
||||
}
|
||||
|
||||
/** 连接层统一错误。 */
|
||||
export class ConnectionError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ConnectionError';
|
||||
}
|
||||
}
|
||||
|
||||
/** 引擎显示名。 */
|
||||
export function engineLabel(kind: EngineKind): string {
|
||||
switch (kind) {
|
||||
case EngineKind.SQLITE:
|
||||
return 'SQLite';
|
||||
case EngineKind.MYSQL:
|
||||
return 'MySQL';
|
||||
case EngineKind.MARIADB:
|
||||
return 'MariaDB';
|
||||
case EngineKind.POSTGRESQL:
|
||||
return 'PostgreSQL';
|
||||
case EngineKind.SQLSERVER:
|
||||
return 'SQL Server';
|
||||
case EngineKind.ORACLE:
|
||||
return 'Oracle';
|
||||
case EngineKind.MONGODB:
|
||||
return 'MongoDB';
|
||||
case EngineKind.REDIS:
|
||||
return 'Redis';
|
||||
case EngineKind.OCEANBASE:
|
||||
return 'OceanBase';
|
||||
case EngineKind.TIDB:
|
||||
return 'TiDB';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/** 状态显示名。 */
|
||||
export function stateLabel(state: ConnectionState): string {
|
||||
switch (state) {
|
||||
case ConnectionState.IDLE:
|
||||
return '空闲';
|
||||
case ConnectionState.CONNECTING:
|
||||
return '连接中';
|
||||
case ConnectionState.CONNECTED:
|
||||
return '已连接';
|
||||
case ConnectionState.DISCONNECTING:
|
||||
return '断开中';
|
||||
case ConnectionState.CLOSED:
|
||||
return '已关闭';
|
||||
case ConnectionState.FAILED:
|
||||
return '连接失败';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Mock 驱动(仅供骨架自检与后续单元测试使用)。
|
||||
*
|
||||
* 行为:所有动作带极小延迟后成功返回,用于验证连接状态机、超时与注册表,
|
||||
* 不产生任何网络 IO。真实驱动见 1.4.3(MySQL/MariaDB)、1.4.7(SQLite)等。
|
||||
*/
|
||||
|
||||
import { ConnectionConfig, Driver, DriverFactory, EngineKind } from '../connection/types';
|
||||
|
||||
export class MockDriver implements Driver {
|
||||
readonly kind: EngineKind;
|
||||
|
||||
constructor(kind: EngineKind) {
|
||||
this.kind = kind;
|
||||
}
|
||||
|
||||
async connect(config: ConnectionConfig): Promise<void> {
|
||||
await MockDriver.delay(30);
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
await MockDriver.delay(10);
|
||||
}
|
||||
|
||||
async ping(): Promise<void> {
|
||||
await MockDriver.delay(5);
|
||||
}
|
||||
|
||||
private static delay(ms: number): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class MockDriverFactory implements DriverFactory {
|
||||
private readonly kind: EngineKind;
|
||||
|
||||
constructor(kind: EngineKind) {
|
||||
this.kind = kind;
|
||||
}
|
||||
|
||||
create(): Driver {
|
||||
return new MockDriver(this.kind);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 统一错误模型(1.4.18)。
|
||||
*
|
||||
* 上层 UI 与日志只认 AppError:稳定错误码、类别、严重度、可翻译 key 与建议。
|
||||
* 各引擎原始错误先经 ErrorMapper 归类,再上抛,避免把厂商错误原文泄露到界面。
|
||||
*/
|
||||
export enum ErrorCategory {
|
||||
CONNECT = 'connect',
|
||||
AUTH = 'auth',
|
||||
PROTOCOL = 'protocol',
|
||||
IO = 'io',
|
||||
LIMIT = 'limit',
|
||||
UNKNOWN = 'unknown'
|
||||
}
|
||||
|
||||
export enum ErrorSeverity {
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal'
|
||||
}
|
||||
|
||||
export class AppError extends Error {
|
||||
readonly code: string;
|
||||
readonly category: ErrorCategory;
|
||||
readonly severity: ErrorSeverity;
|
||||
readonly messageKey: string;
|
||||
readonly suggestion: string;
|
||||
|
||||
constructor(code: string, category: ErrorCategory, severity: ErrorSeverity,
|
||||
messageKey: string, suggestion: string, cause?: string) {
|
||||
super(cause === undefined ? messageKey : cause);
|
||||
this.name = 'AppError';
|
||||
this.code = code;
|
||||
this.category = category;
|
||||
this.severity = severity;
|
||||
this.messageKey = messageKey;
|
||||
this.suggestion = suggestion;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 错误映射表(1.4.18)。
|
||||
*
|
||||
* 把各引擎的原始错误码/文案归类为 AppError。规则按「先精确码、后关键字」排列,
|
||||
* 命中即返回;未命中归为 UNKNOWN 并保留原始文案供诊断日志使用。
|
||||
*
|
||||
* 覆盖范围:网络层(ECONNREFUSED/ETIMEDOUT/ENOTFOUND)、
|
||||
* MySQL(1045/2003/1213)、PostgreSQL(28P01/42501)、Oracle(ORA-01017/ORA-12170)、
|
||||
* 以及通用关键字(access denied / timeout / deadlock)。
|
||||
*/
|
||||
import { AppError, ErrorCategory, ErrorSeverity } from './AppError';
|
||||
|
||||
interface ErrorRule {
|
||||
pattern: string;
|
||||
code: string;
|
||||
category: ErrorCategory;
|
||||
severity: ErrorSeverity;
|
||||
messageKey: string;
|
||||
suggestion: string;
|
||||
}
|
||||
|
||||
export class ErrorMapper {
|
||||
private static readonly RULES: ErrorRule[] = [
|
||||
{
|
||||
pattern: '1045', code: 'KUGE-DB-1045', category: ErrorCategory.AUTH, severity: ErrorSeverity.ERROR,
|
||||
messageKey: 'error.auth.denied', suggestion: '请检查用户名、密码与主机白名单'
|
||||
},
|
||||
{
|
||||
pattern: '28P01', code: 'KUGE-DB-28P01', category: ErrorCategory.AUTH, severity: ErrorSeverity.ERROR,
|
||||
messageKey: 'error.auth.denied', suggestion: '请检查用户名与密码'
|
||||
},
|
||||
{
|
||||
pattern: 'ORA-01017', code: 'KUGE-DB-ORA01017', category: ErrorCategory.AUTH, severity: ErrorSeverity.ERROR,
|
||||
messageKey: 'error.auth.denied', suggestion: '请检查用户名、密码与服务名'
|
||||
},
|
||||
{
|
||||
pattern: '42501', code: 'KUGE-DB-42501', category: ErrorCategory.AUTH, severity: ErrorSeverity.ERROR,
|
||||
messageKey: 'error.auth.insufficient', suggestion: '当前账号权限不足,请联系 DBA 授权'
|
||||
},
|
||||
{
|
||||
pattern: 'ECONNREFUSED', code: 'KUGE-NET-0001', category: ErrorCategory.CONNECT, severity: ErrorSeverity.ERROR,
|
||||
messageKey: 'error.connect.refused', suggestion: '目标端口未监听,请检查主机与端口'
|
||||
},
|
||||
{
|
||||
pattern: 'ENOTFOUND', code: 'KUGE-NET-0002', category: ErrorCategory.CONNECT, severity: ErrorSeverity.ERROR,
|
||||
messageKey: 'error.connect.dns', suggestion: '主机名无法解析,请检查地址或 DNS'
|
||||
},
|
||||
{
|
||||
pattern: 'ORA-12170', code: 'KUGE-DB-ORA12170', category: ErrorCategory.CONNECT, severity: ErrorSeverity.ERROR,
|
||||
messageKey: 'error.connect.timeout', suggestion: '连接超时,请检查网络与防火墙'
|
||||
},
|
||||
{
|
||||
pattern: 'ETIMEDOUT', code: 'KUGE-NET-0003', category: ErrorCategory.IO, severity: ErrorSeverity.WARN,
|
||||
messageKey: 'error.io.timeout', suggestion: '网络较慢或服务繁忙,可稍后重试'
|
||||
},
|
||||
{
|
||||
pattern: '2003', code: 'KUGE-DB-2003', category: ErrorCategory.CONNECT, severity: ErrorSeverity.ERROR,
|
||||
messageKey: 'error.connect.unreachable', suggestion: '无法连接服务器,请检查主机、端口与网络'
|
||||
},
|
||||
{
|
||||
pattern: '1213', code: 'KUGE-DB-1213', category: ErrorCategory.IO, severity: ErrorSeverity.WARN,
|
||||
messageKey: 'error.io.deadlock', suggestion: '发生死锁,事务已回滚,可重试'
|
||||
},
|
||||
{
|
||||
pattern: 'ACCESS DENIED', code: 'KUGE-DB-0001', category: ErrorCategory.AUTH, severity: ErrorSeverity.ERROR,
|
||||
messageKey: 'error.auth.denied', suggestion: '请检查凭据与账号状态'
|
||||
},
|
||||
{
|
||||
pattern: 'TIMEOUT', code: 'KUGE-NET-0004', category: ErrorCategory.IO, severity: ErrorSeverity.WARN,
|
||||
messageKey: 'error.io.timeout', suggestion: '操作超时,请检查网络后重试'
|
||||
},
|
||||
{
|
||||
pattern: 'DEADLOCK', code: 'KUGE-DB-0002', category: ErrorCategory.IO, severity: ErrorSeverity.WARN,
|
||||
messageKey: 'error.io.deadlock', suggestion: '发生死锁,可重试'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* 映射原始错误。
|
||||
* @param rawCode 引擎错误码或网络错误码(可为空)
|
||||
* @param rawMessage 原始错误文案
|
||||
*/
|
||||
static map(rawCode: string, rawMessage: string): AppError {
|
||||
const haystack: string = (rawCode + ' ' + rawMessage).toUpperCase();
|
||||
for (let i = 0; i < ErrorMapper.RULES.length; i++) {
|
||||
const rule: ErrorRule = ErrorMapper.RULES[i];
|
||||
if (haystack.indexOf(rule.pattern) >= 0) {
|
||||
return new AppError(rule.code, rule.category, rule.severity, rule.messageKey, rule.suggestion, rawMessage);
|
||||
}
|
||||
}
|
||||
return new AppError('KUGE-UNKNOWN-0000', ErrorCategory.UNKNOWN, ErrorSeverity.ERROR,
|
||||
'error.unknown', '请查看诊断日志或联系支持', rawMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 引擎插件契约(1.2.9 / 1.3.4)。
|
||||
*
|
||||
* 每个 engine-<kind> HAR 导出一个 EnginePlugin;装配层只认这个契约。
|
||||
* meta.apiVersion 表示插件依赖的 engine-api 版本,由注册表校验。
|
||||
*/
|
||||
import { Driver, EngineKind } from '../connection/types';
|
||||
|
||||
/** 插件元信息。 */
|
||||
export interface PluginMeta {
|
||||
/** 模块名,如 engine-sqlite。 */
|
||||
readonly name: string;
|
||||
/** 插件自身版本(SemVer)。 */
|
||||
readonly version: string;
|
||||
/** 依赖的 engine-api 版本(SemVer)。 */
|
||||
readonly apiVersion: string;
|
||||
}
|
||||
|
||||
/** 引擎插件。 */
|
||||
export interface EnginePlugin {
|
||||
readonly kind: EngineKind;
|
||||
readonly meta: PluginMeta;
|
||||
createDriver(): Driver;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 引擎插件注册表(1.3.4)。
|
||||
*
|
||||
* 职责:
|
||||
* 1. 注册引擎插件,拒绝重复引擎与不兼容的 engine-api 版本;
|
||||
* 2. 提供查询与列举,供 ConnectionManager / 装配层使用;
|
||||
* 3. 记录注册顺序,便于诊断输出。
|
||||
*
|
||||
* 加载方式:装配层(entry/app)显式 import 各引擎插件并调用 register;
|
||||
* 按需分发的引擎在模块安装完成后注册(ADR-004,拟议)。
|
||||
*/
|
||||
import { EngineKind } from '../connection/types';
|
||||
import { EnginePlugin } from './EnginePlugin';
|
||||
import { SemVerUtil } from './SemVerUtil';
|
||||
|
||||
/** 当前 engine-api 版本;契约变化时同步升级(见 ADR-003)。 */
|
||||
export const ENGINE_API_VERSION: string = '1.0.0';
|
||||
|
||||
export class EngineRegistry {
|
||||
private plugins: Map<EngineKind, EnginePlugin> = new Map<EngineKind, EnginePlugin>();
|
||||
private order: EngineKind[] = [];
|
||||
|
||||
/**
|
||||
* 注册插件。重复引擎或版本不兼容时抛错,避免装配期静默覆盖。
|
||||
*/
|
||||
register(plugin: EnginePlugin): void {
|
||||
if (this.plugins.has(plugin.kind)) {
|
||||
throw new Error('duplicate engine plugin: ' + plugin.kind + ' (' + plugin.meta.name + ')');
|
||||
}
|
||||
if (!SemVerUtil.isCompatible(plugin.meta.apiVersion, ENGINE_API_VERSION)) {
|
||||
throw new Error('incompatible engine-api version: ' + plugin.meta.name + ' requires ' +
|
||||
plugin.meta.apiVersion + ', current ' + ENGINE_API_VERSION);
|
||||
}
|
||||
this.plugins.set(plugin.kind, plugin);
|
||||
this.order.push(plugin.kind);
|
||||
}
|
||||
|
||||
get(kind: EngineKind): EnginePlugin | undefined {
|
||||
return this.plugins.get(kind);
|
||||
}
|
||||
|
||||
has(kind: EngineKind): boolean {
|
||||
return this.plugins.has(kind);
|
||||
}
|
||||
|
||||
/** 按注册顺序返回插件名,用于诊断日志。 */
|
||||
names(): string[] {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < this.order.length; i++) {
|
||||
const plugin = this.plugins.get(this.order[i]);
|
||||
if (plugin !== undefined) {
|
||||
out.push(plugin.meta.name + '@' + plugin.meta.version);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this.order.length;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.plugins.clear();
|
||||
this.order = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 语义化版本工具(1.3.4)。
|
||||
*
|
||||
* 只实现插件兼容性判断所需的最小子集:解析 major.minor.patch 与兼容性比较。
|
||||
* 纯函数,不依赖平台 API,可独立测试。
|
||||
*/
|
||||
export class SemVerUtil {
|
||||
/** 解析版本号;非法返回 undefined。 */
|
||||
static parse(version: string): number[] | undefined {
|
||||
if (version.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const parts: string[] = version.split('.');
|
||||
if (parts.length !== 3) {
|
||||
return undefined;
|
||||
}
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const text: string = parts[i];
|
||||
if (text.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
for (let j = 0; j < text.length; j++) {
|
||||
const code: number = text.charCodeAt(j);
|
||||
if (code < 48 || code > 57) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
out.push(Number(text));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件要求的 API 版本是否可被当前版本满足。
|
||||
* 规则:主版本必须相同(不兼容变更需升主版本);要求的 minor/patch 不得高于当前。
|
||||
*/
|
||||
static isCompatible(required: string, current: string): boolean {
|
||||
const req: number[] | undefined = SemVerUtil.parse(required);
|
||||
const cur: number[] | undefined = SemVerUtil.parse(current);
|
||||
if (req === undefined || cur === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (req[0] !== cur[0]) {
|
||||
return false;
|
||||
}
|
||||
if (req[1] > cur[1]) {
|
||||
return false;
|
||||
}
|
||||
if (req[1] === cur[1] && req[2] > cur[2]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 协议帧编解码基座(1.4.11)。
|
||||
*
|
||||
* 帧格式(小端):
|
||||
* [0..3] uint32 整帧长度(含头,6 + payload)
|
||||
* [4..5] uint16 序号
|
||||
* [6..] bytes 载荷
|
||||
*
|
||||
* 该格式与 MySQL / MariaDB 的 3 字节长度头不同,是引擎无关的内部封装层:
|
||||
* 各引擎协议实现(1.4.3–1.4.10)在载荷里携带自己的报文,本层只负责
|
||||
* 传输分帧、粘包拆分与错误长度防御。
|
||||
*/
|
||||
|
||||
import { ConnectionError } from '../connection/types';
|
||||
|
||||
/** 一帧数据。 */
|
||||
export class Frame {
|
||||
readonly seq: number;
|
||||
readonly payload: Uint8Array;
|
||||
|
||||
constructor(seq: number, payload: Uint8Array) {
|
||||
this.seq = seq;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
export class FrameCodec {
|
||||
/** 头长度:4 字节长度 + 2 字节序号。 */
|
||||
static readonly HEADER_BYTES: number = 6;
|
||||
/** 单帧载荷上限:16 MB。 */
|
||||
static readonly MAX_PAYLOAD_BYTES: number = 16 * 1024 * 1024;
|
||||
|
||||
/** 组帧。载荷超限时抛 ConnectionError。 */
|
||||
static encode(seq: number, payload: Uint8Array): Uint8Array {
|
||||
if (payload.length > FrameCodec.MAX_PAYLOAD_BYTES) {
|
||||
throw new ConnectionError('payload too large: ' + payload.length);
|
||||
}
|
||||
const total: number = FrameCodec.HEADER_BYTES + payload.length;
|
||||
const out: Uint8Array = new Uint8Array(total);
|
||||
out[0] = total & 0xFF;
|
||||
out[1] = (total >>> 8) & 0xFF;
|
||||
out[2] = (total >>> 16) & 0xFF;
|
||||
out[3] = (total >>> 24) & 0xFF;
|
||||
out[4] = seq & 0xFF;
|
||||
out[5] = (seq >>> 8) & 0xFF;
|
||||
out.set(payload, FrameCodec.HEADER_BYTES);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 读取头部声明的整帧长度;不足 4 字节返回 -1。 */
|
||||
static frameLength(header: Uint8Array): number {
|
||||
if (header.length < 4) {
|
||||
return -1;
|
||||
}
|
||||
return header[0] | (header[1] << 8) | (header[2] << 16) | (header[3] << 24);
|
||||
}
|
||||
|
||||
/** 解出第一帧;数据不足返回 undefined(由调用方继续收)。 */
|
||||
static decode(buffer: Uint8Array): Frame | undefined {
|
||||
const total: number = FrameCodec.frameLength(buffer);
|
||||
if (total < 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (total < FrameCodec.HEADER_BYTES || total > FrameCodec.HEADER_BYTES + FrameCodec.MAX_PAYLOAD_BYTES) {
|
||||
throw new ConnectionError('invalid frame length: ' + total);
|
||||
}
|
||||
if (buffer.length < total) {
|
||||
return undefined;
|
||||
}
|
||||
const seq: number = buffer[4] | (buffer[5] << 8);
|
||||
return new Frame(seq, buffer.slice(FrameCodec.HEADER_BYTES, total));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "engine",
|
||||
"type": "har",
|
||||
"deviceTypes": [
|
||||
"tablet",
|
||||
"2in1"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"name": "engine_module_desc",
|
||||
"value": "库阁连接层:连接抽象、协议帧编解码与引擎驱动"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user