初始化项目

This commit is contained in:
2026-10-19 00:11:38 +08:00
commit 9545e48e4d
150 changed files with 9559 additions and 0 deletions
+17
View File
@@ -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;
}
+16
View File
@@ -0,0 +1,16 @@
/**
* @kuge/core 的唯一公开入口。
*
* 约定(见 docs/adr/ADR-001):HAR 模块只通过本文件暴露能力,
* 外部模块禁止直接 import src/main/ets 下的实现文件。
*/
export { Logger, LogLevel, LOG_DOMAIN, LogSink } from './src/main/ets/logger/Logger';
export { FileLogSink } from './src/main/ets/logger/FileLogSink';
export { AppConfig } from './src/main/ets/config/AppConfig';
export { AppInfo } from './src/main/ets/app/AppInfo';
export { TaskScheduler, TaskRunner, TaskCancelledError } from './src/main/ets/task/TaskScheduler';
export { LruCache } from './src/main/ets/storage/LruCache';
export { KeyValueStore, MemoryKeyValueStore } from './src/main/ets/storage/KeyValueStore';
export { CredentialStore, CredentialVault, KeyProvider, InMemoryKeyProvider } from './src/main/ets/security/CredentialVault';
export { I18nUtil } from './src/main/ets/i18n/I18n';
export { DiagnosticBuilder, DiagnosticContext, Redactor } from './src/main/ets/diagnostics/DiagnosticBuilder';
+9
View File
@@ -0,0 +1,9 @@
{
"apiType": "stageMode",
"buildOption": {},
"targets": [
{
"name": "default"
}
]
}
+6
View File
@@ -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. */
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "@kuge/core",
"version": "1.0.0",
"description": "Kuge core capabilities: logging, app info, shared types",
"main": "Index.ets",
"author": "Kuge Team",
"license": "Apache-2.0",
"dependencies": {}
}
+20
View File
@@ -0,0 +1,20 @@
/**
* 应用级静态信息。
*
* 仅存放不随构建变化的产品标识;构建期注入的信息(版本号、构建号)
* 由 AppScope/app.json5 与 BuildProfile 提供,不在此重复维护。
*/
export class AppInfo {
/** 产品中文名。 */
static readonly NAME: string = '库阁';
/** 产品英文名(拼音音译)。 */
static readonly ENGLISH_NAME: string = 'Kuge';
/** 一句话定位。 */
static readonly TAGLINE: string = '跨库数据库工作台';
/** 目标平台。 */
static readonly PLATFORM: string = 'HarmonyOS NEXT';
/** 目标设备形态。 */
static readonly FORM_FACTORS: string = 'PC / 平板';
/** 当前迭代版本(与 app.json5 的 versionName 保持同步)。 */
static readonly VERSION: string = '0.1.0';
}
+91
View File
@@ -0,0 +1,91 @@
/**
* 应用配置(持久化)。
*
* 使用 Preferences 保存少量启动即读的配置项;业务级配置(连接、外观等)
* 在后续工作包中按领域拆到各自模块,不堆在同一个 key 空间里。
*
* 约定:配置读写同步完成(Preferences 的 Sync 接口),避免启动阶段异步竞态;
* 落盘用 flush() 异步提交,不阻塞 UI 线程。存储不可用时退化为内存默认值,
* 不阻断应用启动。
*/
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { LogLevel } from '../logger/Logger';
/** Preferences 文件名。 */
const STORE_NAME: string = 'kuge_settings';
/** 日志级别键。 */
const KEY_LOG_LEVEL: string = 'log.level';
export class AppConfig {
private static store: preferences.Preferences | undefined = undefined;
private static logLevel: LogLevel = LogLevel.INFO;
/** 在 Ability 启动时调用一次;重复调用无副作用。 */
static init(context: common.Context): void {
if (AppConfig.store !== undefined) {
return;
}
try {
const options: preferences.Options = { name: STORE_NAME };
const store: preferences.Preferences = preferences.getPreferencesSync(context, options);
const raw: preferences.ValueType = store.getSync(KEY_LOG_LEVEL, 'info');
AppConfig.store = store;
AppConfig.logLevel = AppConfig.parseLevel(typeof raw === 'string' ? raw : 'info');
} catch (err) {
// 存储不可用(首次启动异常、配额受限等)时使用默认值,不影响主流程。
AppConfig.store = undefined;
AppConfig.logLevel = LogLevel.INFO;
}
}
static getLogLevel(): LogLevel {
return AppConfig.logLevel;
}
/** 修改并持久化日志级别。 */
static setLogLevel(level: LogLevel): void {
AppConfig.logLevel = level;
const store = AppConfig.store;
if (store === undefined) {
return;
}
try {
store.putSync(KEY_LOG_LEVEL, AppConfig.levelName(level));
store.flush();
} catch (err) {
// 持久化失败时保留内存值,下次启动回落到旧值。
}
}
/** 级别名(用于持久化与界面展示)。 */
static levelName(level: LogLevel): string {
switch (level) {
case LogLevel.DEBUG:
return 'debug';
case LogLevel.INFO:
return 'info';
case LogLevel.WARN:
return 'warn';
case LogLevel.ERROR:
return 'error';
default:
return 'info';
}
}
private static parseLevel(raw: string): LogLevel {
switch (raw) {
case 'debug':
return LogLevel.DEBUG;
case 'warn':
return LogLevel.WARN;
case 'error':
return LogLevel.ERROR;
case 'info':
default:
return LogLevel.INFO;
}
}
}
@@ -0,0 +1,66 @@
/**
* 诊断信息构建与脱敏(1.3.10)。
*
* 职责:
* 1. 汇总应用/平台/版本/时间与日志尾部,生成可读的诊断文本;
* 2. 对敏感字段做脱敏(密码、令牌、密钥、鉴权头),保证诊断包可安全外发;
* 3. 崩溃处理与 faultlogger 接入在平台适配器中完成,本文件保持纯逻辑、可测试。
*/
/** 诊断上下文。 */
export interface DiagnosticContext {
appName: string;
version: string;
platform: string;
formFactors: string;
logLevel: string;
}
/** 脱敏器:按常见敏感键名替换值。 */
export class Redactor {
/** 需要脱敏的键名(小写比较)。 */
private static readonly KEYS: string[] = [
'password', 'passwd', 'pwd', 'secret', 'token', 'apikey', 'api_key', 'authorization', 'credential'
];
/** 把 key=value / key: value 形式的值替换为 ***。 */
static redact(text: string): string {
let out: string = text;
for (let i = 0; i < Redactor.KEYS.length; i++) {
const key: string = Redactor.KEYS[i];
const pattern: RegExp = new RegExp('(' + key + '\\s*[=:]\\s*)([^\\s,;]+)', 'gi');
out = out.replace(pattern, '$1***');
}
// 鉴权头整行脱敏(可能含 "Bearer xxx" 等前缀)
out = out.replace(new RegExp('(authorization\\s*[=:]\\s*)[^\\r\\n]*', 'gi'), '$1***');
// 裸 Bearer 令牌
out = out.replace(new RegExp('(bearer\\s+)[A-Za-z0-9._\\-]+', 'gi'), '$1***');
return out;
}
}
/** 诊断包构建器。 */
export class DiagnosticBuilder {
private readonly ctx: DiagnosticContext;
constructor(ctx: DiagnosticContext) {
this.ctx = ctx;
}
/**
* 生成诊断文本。
* @param logTail 日志尾部(已读取的内容)
* @param timestamp ISO 时间字符串,由调用方提供,避免依赖本地时区
*/
build(logTail: string, timestamp: string): string {
const lines: string[] = [];
lines.push('=== 库阁诊断信息 ===');
lines.push('应用: ' + this.ctx.appName + ' ' + this.ctx.version);
lines.push('平台: ' + this.ctx.platform + ' (' + this.ctx.formFactors + ')');
lines.push('日志级别: ' + this.ctx.logLevel);
lines.push('时间: ' + timestamp);
lines.push('--- 日志尾部 ---');
lines.push(Redactor.redact(logTail));
return lines.join('\n');
}
}
+77
View File
@@ -0,0 +1,77 @@
/**
* 多语言运行时(1.3.9)。
*
* 纯逻辑实现:语言标签归一化、支持列表匹配、用户偏好与系统语言的协商。
* 资源文件仍由 resources/<语言>/element/string.json 提供,本类只决定「用哪个语言」。
*
* 协商顺序(对应 I18N-001 设计):
* 1) 用户显式选择且在支持列表内 → 用它;
* 2) 否则取系统语言,按「完全匹配 → 主语言匹配」找支持项;
* 3) 都不匹配 → 回退 fallback(中文简体)。
*
* 标签规范:小写化并用 '-' 分隔,如 zh-Hans-CN、en-US;忽略大小写与下划线差异。
*/
export class I18nUtil {
/** 归一化语言标签:小写、下划线转连字符、去空白。 */
static normalize(tag: string): string {
return tag.trim().toLowerCase().replace('_', '-');
}
/** 主语言(第一个子标签),如 zh-Hans-CN → zh。 */
static primaryOf(tag: string): string {
const normalized: string = I18nUtil.normalize(tag);
const idx: number = normalized.indexOf('-');
return idx < 0 ? normalized : normalized.substring(0, idx);
}
/** 是否在支持列表中(完全匹配,忽略大小写)。 */
static isSupported(tag: string, supported: string[]): boolean {
const target: string = I18nUtil.normalize(tag);
for (let i = 0; i < supported.length; i++) {
if (I18nUtil.normalize(supported[i]) === target) {
return true;
}
}
return false;
}
/** 在支持列表中找到与给定标签最匹配的项;找不到返回 undefined。 */
static match(tag: string, supported: string[]): string | undefined {
const target: string = I18nUtil.normalize(tag);
for (let i = 0; i < supported.length; i++) {
if (I18nUtil.normalize(supported[i]) === target) {
return supported[i];
}
}
const primary: string = I18nUtil.primaryOf(target);
for (let i = 0; i < supported.length; i++) {
if (I18nUtil.primaryOf(supported[i]) === primary) {
return supported[i];
}
}
return undefined;
}
/**
* 协商最终语言。
* @param systemTag 系统语言,如 zh-Hans-CN
* @param userTag 用户偏好(空表示跟随系统)
* @param supported 支持列表,顺序即优先级
* @param fallback 回退语言
*/
static negotiate(systemTag: string, userTag: string, supported: string[], fallback: string): string {
if (userTag.length > 0) {
const byUser: string | undefined = I18nUtil.match(userTag, supported);
if (byUser !== undefined) {
return byUser;
}
}
if (systemTag.length > 0) {
const bySystem: string | undefined = I18nUtil.match(systemTag, supported);
if (bySystem !== undefined) {
return bySystem;
}
}
return fallback;
}
}
+139
View File
@@ -0,0 +1,139 @@
/**
* 文件日志落地。
*
* 只把 WARN 及以上写入文件,控制 IO 量与磁盘占用;完整日志仍在 hilog。
* 单文件超过上限时截断重写(保留最近一个窗口),后续接入归档与诊断包导出
* 时再替换为滚动策略(对应 1.3.10 崩溃上报与诊断打包)。
*
* 所有文件操作都在内部吞掉异常:日志系统自身的失败不得影响业务。
*/
import { fileIo as fs } from '@kit.CoreFileKit';
import { LogLevel, LogSink } from './Logger';
/** 单文件上限:512 KB。 */
const MAX_FILE_BYTES: number = 512 * 1024;
export class FileLogSink implements LogSink {
private readonly filePath: string;
private readonly dirPath: string;
private file: fs.File | undefined = undefined;
constructor(dirPath: string) {
this.dirPath = dirPath;
this.filePath = dirPath + '/kuge.log';
try {
if (!fs.accessSync(dirPath)) {
fs.mkdirSync(dirPath);
}
} catch (err) {
// 目录创建失败时仍尝试直接打开文件(目录可能已存在)。
}
this.file = this.openAppendSafe();
}
/** 只落 WARN / ERROR,避免 DEBUG 日志把文件刷满。 */
write(level: LogLevel, tag: string, message: string): void {
if (level < LogLevel.WARN) {
return;
}
try {
this.rotateIfNeeded();
const file = this.file;
if (file === undefined) {
return;
}
const line: string = this.timestamp() + ' [' + this.levelTag(level) + '] [' + tag + '] ' + message + '\n';
fs.writeSync(file.fd, line);
} catch (err) {
// 忽略写入失败,避免日志系统本身造成业务中断。
}
}
/** 读取最近 maxLines 行,用于诊断面板与问题反馈。 */
readTail(maxLines: number): string {
try {
const all: string = fs.readTextSync(this.filePath);
const lines: string[] = all.split('\n');
const start: number = Math.max(0, lines.length - maxLines);
return lines.slice(start).join('\n');
} catch (err) {
return '';
}
}
close(): void {
const file = this.file;
if (file === undefined) {
return;
}
try {
fs.closeSync(file);
} catch (err) {
// 忽略关闭异常
}
this.file = undefined;
}
private openAppendSafe(): fs.File | undefined {
try {
return fs.openSync(this.filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.APPEND);
} catch (err) {
return undefined;
}
}
private rotateIfNeeded(): void {
const file = this.file;
if (file === undefined) {
return;
}
try {
const size: number = fs.statSync(this.filePath).size;
if (size <= MAX_FILE_BYTES) {
return;
}
fs.closeSync(file);
this.file = undefined;
const fresh: fs.File = fs.openSync(this.filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);
fs.closeSync(fresh);
this.file = this.openAppendSafe();
} catch (err) {
// 截断失败时保持原文件继续追加,避免丢日志。
}
}
private levelTag(level: LogLevel): string {
switch (level) {
case LogLevel.WARN:
return 'W';
case LogLevel.ERROR:
return 'E';
case LogLevel.INFO:
return 'I';
default:
return 'D';
}
}
private timestamp(): string {
const d: Date = new Date();
return d.getFullYear() + '-' + this.pad(d.getMonth() + 1) + '-' + this.pad(d.getDate()) +
' ' + this.pad(d.getHours()) + ':' + this.pad(d.getMinutes()) + ':' + this.pad(d.getSeconds()) +
'.' + this.pad3(d.getMilliseconds());
}
private pad(n: number): string {
return n < 10 ? '0' + n : '' + n;
}
private pad3(n: number): string {
if (n < 10) {
return '00' + n;
}
if (n < 100) {
return '0' + n;
}
return '' + n;
}
}
+108
View File
@@ -0,0 +1,108 @@
/**
* 统一日志出口。
*
* 业务代码不直接调用 hilog,统一走 Logger,保证:
* 1. 域号(domain)唯一且集中分配;
* 2. tag 由模块名派生,便于过滤;
* 3. 级别可在运行期统一收敛(发布版默认 WARN);
* 4. 可选挂接 LogSink(如文件落地),实现与 hilog 同源的诊断日志。
*/
import { hilog } from '@kit.PerformanceAnalysisKit';
/** hilog 域号,取值范围 0x0000–0xFFFF,按业务域集中分配。 */
export const LOG_DOMAIN: number = 0xDB01;
/** 日志级别,数值越大越严重。 */
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3
}
/** 日志落地面(文件、内存缓冲、远程上报等按此接口扩展)。 */
export interface LogSink {
write(level: LogLevel, tag: string, message: string): void;
}
/** 统一日志器。按模块创建实例后复用,避免重复分配。 */
export class Logger {
private static minLevel: LogLevel = LogLevel.INFO;
private static sink: LogSink | undefined = undefined;
private readonly tag: string;
private constructor(tag: string) {
this.tag = tag;
}
/** 创建指定模块的日志器。 */
static create(tag: string): Logger {
return new Logger(tag);
}
/** 设置全局最低输出级别,发布版应设为 WARN 及以上。 */
static setMinLevel(level: LogLevel): void {
Logger.minLevel = level;
}
static getMinLevel(): LogLevel {
return Logger.minLevel;
}
/** 挂接落地面;传 undefined 表示取消。 */
static setSink(sink: LogSink | undefined): void {
Logger.sink = sink;
}
/** 用 {0}、{1} 占位符拼接消息,避免业务侧自行拼字符串。 */
private format(message: string, args: string[]): string {
let text: string = message;
for (let i = 0; i < args.length; i++) {
text = text.replace('{' + i + '}', args[i]);
}
return text;
}
private emit(level: LogLevel, message: string, args: string[]): void {
if (level < Logger.minLevel) {
return;
}
const text: string = this.format(message, args);
switch (level) {
case LogLevel.DEBUG:
hilog.debug(LOG_DOMAIN, this.tag, '%{public}s', text);
break;
case LogLevel.INFO:
hilog.info(LOG_DOMAIN, this.tag, '%{public}s', text);
break;
case LogLevel.WARN:
hilog.warn(LOG_DOMAIN, this.tag, '%{public}s', text);
break;
case LogLevel.ERROR:
default:
hilog.error(LOG_DOMAIN, this.tag, '%{public}s', text);
break;
}
const sink = Logger.sink;
if (sink !== undefined) {
sink.write(level, this.tag, text);
}
}
debug(message: string, ...args: string[]): void {
this.emit(LogLevel.DEBUG, message, args);
}
info(message: string, ...args: string[]): void {
this.emit(LogLevel.INFO, message, args);
}
warn(message: string, ...args: string[]): void {
this.emit(LogLevel.WARN, message, args);
}
error(message: string, ...args: string[]): void {
this.emit(LogLevel.ERROR, message, args);
}
}
@@ -0,0 +1,123 @@
/**
* 凭据保险箱(1.3.8)。
*
* 结构:
* CredentialVault —— 编排层:确保密钥存在、加密后写入 KeyValueStore、读取时解密;
* KeyProvider —— 密钥与加解密能力抽象,HUKS 实现在平台适配器中提供;
* InMemoryKeyProvider —— 测试/降级实现(仅做可逆标记,不具备保密性,禁止用于生产)。
*
* 约束(对应 SEC-001 设计):
* - 明文只在 put/get 的调用栈中存在,不写入日志、不进入界面状态;
* - 密钥别名按保险箱实例隔离;
* - 加密失败不得降级为明文。
*/
import { KeyValueStore } from '../storage/KeyValueStore';
/** 凭据条目前缀,便于按域清理。 */
const CRED_PREFIX: string = 'cred.';
/** 密钥提供者:生成/存在性/删除/加解密。 */
export interface KeyProvider {
generate(keyAlias: string): Promise<void>;
has(keyAlias: string): Promise<boolean>;
delete(keyAlias: string): Promise<void>;
encrypt(keyAlias: string, plain: string): Promise<string>;
decrypt(keyAlias: string, cipher: string): Promise<string>;
}
/** 凭据存储契约。 */
export interface CredentialStore {
put(ref: string, secret: string): Promise<void>;
get(ref: string): Promise<string | undefined>;
has(ref: string): Promise<boolean>;
remove(ref: string): Promise<void>;
clear(): Promise<void>;
}
/** 测试用密钥提供者:可逆标记,不具备安全性。 */
export class InMemoryKeyProvider implements KeyProvider {
private readonly keys: Set<string> = new Set<string>();
async generate(keyAlias: string): Promise<void> {
this.keys.add(keyAlias);
}
async has(keyAlias: string): Promise<boolean> {
return this.keys.has(keyAlias);
}
async delete(keyAlias: string): Promise<void> {
this.keys.delete(keyAlias);
}
async encrypt(keyAlias: string, plain: string): Promise<string> {
if (!this.keys.has(keyAlias)) {
throw new Error('key not found: ' + keyAlias);
}
return 'enc:' + plain;
}
async decrypt(keyAlias: string, cipher: string): Promise<string> {
if (!this.keys.has(keyAlias)) {
throw new Error('key not found: ' + keyAlias);
}
if (cipher.indexOf('enc:') !== 0) {
throw new Error('invalid cipher text');
}
return cipher.substring(4);
}
}
export class CredentialVault implements CredentialStore {
private readonly store: KeyValueStore;
private readonly provider: KeyProvider;
private readonly keyAlias: string;
constructor(store: KeyValueStore, provider: KeyProvider, keyAlias: string) {
if (keyAlias.length === 0) {
throw new Error('keyAlias must not be empty');
}
this.store = store;
this.provider = provider;
this.keyAlias = keyAlias;
}
async put(ref: string, secret: string): Promise<void> {
if (ref.length === 0) {
throw new Error('credential ref must not be empty');
}
if (secret.length === 0) {
throw new Error('credential secret must not be empty');
}
if (!(await this.provider.has(this.keyAlias))) {
await this.provider.generate(this.keyAlias);
}
const cipher: string = await this.provider.encrypt(this.keyAlias, secret);
await this.store.put(CRED_PREFIX + ref, cipher);
}
async get(ref: string): Promise<string | undefined> {
const cipher: string | undefined = await this.store.get(CRED_PREFIX + ref);
if (cipher === undefined) {
return undefined;
}
return this.provider.decrypt(this.keyAlias, cipher);
}
async has(ref: string): Promise<boolean> {
return (await this.store.get(CRED_PREFIX + ref)) !== undefined;
}
async remove(ref: string): Promise<void> {
await this.store.delete(CRED_PREFIX + ref);
}
async clear(): Promise<void> {
const keys: string[] = await this.store.keys();
for (let i = 0; i < keys.length; i++) {
if (keys[i].indexOf(CRED_PREFIX) === 0) {
await this.store.delete(keys[i]);
}
}
}
}
@@ -0,0 +1,44 @@
/**
* 键值存储抽象(1.3.7)。
*
* 平台实现(Preferences / RDB / 文件)在后续适配器中提供;本文件只定义契约与
* 内存实现,便于在无平台环境测试业务逻辑,也便于替换存储介质。
*
* 约定:键使用命名空间前缀(如 conn./meta./ui.),便于按域清理与迁移。
*/
export interface KeyValueStore {
get(key: string): Promise<string | undefined>;
put(key: string, value: string): Promise<void>;
delete(key: string): Promise<void>;
keys(): Promise<string[]>;
clear(): Promise<void>;
}
/** 内存实现:仅用于测试与降级(不可持久化)。 */
export class MemoryKeyValueStore implements KeyValueStore {
private readonly data: Map<string, string> = new Map<string, string>();
async get(key: string): Promise<string | undefined> {
return this.data.get(key);
}
async put(key: string, value: string): Promise<void> {
this.data.set(key, value);
}
async delete(key: string): Promise<void> {
this.data.delete(key);
}
async keys(): Promise<string[]> {
const out: string[] = [];
this.data.forEach((_value: string, key: string) => {
out.push(key);
});
return out;
}
async clear(): Promise<void> {
this.data.clear();
}
}
+86
View File
@@ -0,0 +1,86 @@
/**
* LRU 缓存(1.3.7)。
*
* 用途:对象元数据、查询结果分页、连接信息等热点数据的本地淘汰缓存。
* 纯逻辑实现,不依赖平台 API;上层可用 Preferences / RDB / 文件做持久化适配。
*
* 语义:get 命中会把条目移到最近使用位置;put 超出容量时淘汰最久未使用项。
*/
export class LruCache<K, V> {
private readonly capacity: number;
private readonly map: Map<K, V> = new Map<K, V>();
private order: K[] = [];
constructor(capacity: number) {
if (capacity < 1) {
throw new Error('LruCache capacity must be >= 1');
}
this.capacity = capacity;
}
get(key: K): V | undefined {
if (!this.map.has(key)) {
return undefined;
}
this.touch(key);
return this.map.get(key);
}
put(key: K, value: V): void {
if (this.map.has(key)) {
this.map.set(key, value);
this.touch(key);
return;
}
this.map.set(key, value);
this.order.push(key);
while (this.order.length > this.capacity) {
const oldest: K | undefined = this.order.shift();
if (oldest !== undefined) {
this.map.delete(oldest);
}
}
}
has(key: K): boolean {
return this.map.has(key);
}
delete(key: K): boolean {
if (!this.map.has(key)) {
return false;
}
this.map.delete(key);
const idx: number = this.order.indexOf(key);
if (idx >= 0) {
this.order.splice(idx, 1);
}
return true;
}
size(): number {
return this.order.length;
}
clear(): void {
this.map.clear();
this.order = [];
}
/** 按最近使用顺序返回键(最旧在前),用于诊断。 */
keys(): K[] {
const out: K[] = [];
for (let i = 0; i < this.order.length; i++) {
out.push(this.order[i]);
}
return out;
}
private touch(key: K): void {
const idx: number = this.order.indexOf(key);
if (idx >= 0) {
this.order.splice(idx, 1);
}
this.order.push(key);
}
}
+114
View File
@@ -0,0 +1,114 @@
/**
* 任务调度框架(1.3.5)。
*
* 目标:把「并发上限、排队、取消、失败传播」收敛到一处,业务侧只提交任务。
* 执行体抽象为 TaskRunner,UI 层与 Worker/TaskPool 的差异由实现方注入,
* 便于在无平台环境做单元测试,也便于后续替换为 TaskPool 的真实执行器。
*
* 语义:
* - submit 返回的 Promise 在任务成功时 resolve,失败或取消时 reject;
* - 取消仅在任务尚未开始(排队中)时生效,返回 true;运行中的任务需自行响应取消信号;
* - 并发达到上限时排队,任务完成后自动补位。
*/
/** 可执行任务。 */
export interface TaskRunner {
run(): Promise<void>;
}
/** 取消错误。 */
export class TaskCancelledError extends Error {
constructor(message: string) {
super(message);
this.name = 'TaskCancelledError';
}
}
/** 排队中的任务项。 */
class QueuedTask {
readonly id: string;
readonly runner: TaskRunner;
readonly resolve: (value: void) => void;
readonly reject: (reason: Error) => void;
constructor(id: string, runner: TaskRunner, resolve: (value: void) => void, reject: (reason: Error) => void) {
this.id = id;
this.runner = runner;
this.resolve = resolve;
this.reject = reject;
}
}
export class TaskScheduler {
private readonly maxConcurrency: number;
private readonly queue: QueuedTask[] = [];
private readonly runningIds: Set<string> = new Set<string>();
private runningCount: number = 0;
constructor(maxConcurrency: number) {
if (maxConcurrency < 1) {
throw new Error('maxConcurrency must be >= 1');
}
this.maxConcurrency = maxConcurrency;
}
/** 提交任务;返回完成 Promise。 */
submit(id: string, runner: TaskRunner): Promise<void> {
if (this.runningIds.has(id)) {
return Promise.reject(new Error('duplicate running task id: ' + id));
}
for (let i = 0; i < this.queue.length; i++) {
if (this.queue[i].id === id) {
return Promise.reject(new Error('duplicate queued task id: ' + id));
}
}
return new Promise<void>((resolve, reject) => {
this.queue.push(new QueuedTask(id, runner, resolve, reject));
this.pump();
});
}
/** 取消排队中的任务;返回是否取消成功。 */
cancel(id: string): boolean {
for (let i = 0; i < this.queue.length; i++) {
if (this.queue[i].id === id) {
const task: QueuedTask = this.queue[i];
this.queue.splice(i, 1);
task.reject(new TaskCancelledError('task cancelled: ' + id));
return true;
}
}
return false;
}
pendingCount(): number {
return this.queue.length;
}
activeCount(): number {
return this.runningCount;
}
private pump(): void {
while (this.runningCount < this.maxConcurrency && this.queue.length > 0) {
const task: QueuedTask = this.queue.shift() as QueuedTask;
this.runningCount++;
this.runningIds.add(task.id);
task.runner.run()
.then(() => {
this.finish(task.id);
task.resolve();
})
.catch((err: Error) => {
this.finish(task.id);
task.reject(err);
});
}
}
private finish(id: string): void {
this.runningIds.delete(id);
this.runningCount--;
this.pump();
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"module": {
"name": "core",
"type": "har",
"deviceTypes": [
"tablet",
"2in1"
]
}
}
@@ -0,0 +1,8 @@
{
"string": [
{
"name": "core_module_desc",
"value": "DBMGR 核心能力模块(日志、应用信息、公共类型)"
}
]
}