import http from '@ohos.net.http';
import { WarmupManifest } from './WarmupManifest';
/**
* manifest 就绪回调。`versionChanged` 表示本次拉取的 version 与旧缓存不同。
*/
export type ManifestReadyCallback = (manifest: WarmupManifest, versionChanged: boolean) => void;
/**
* manifest 拉取 / 缓存 / 新鲜度管理(进程内单例语义,由 Prewarmer 持有)。
*
* 策略(design.md Decision 5):
* - 内存缓存整个 session;TTL 读自响应头 `Cache-Control: max-age`,缺省 300s。
* - `ensureFresh`:缺失则拉取并排队回调;已有但过期则后台重拉(stale-while-revalidate)。
* - `isFetching` 保护:每 TTL 窗口最多一次重拉。
* - 拉取失败静默 no-op。
*/
export class ManifestStore {
/** pending 回调队列上限。弱网下 ensureFresh 可能多次入队,设上限防御异常累积。 */
private static readonly MAX_PENDING: number = 16;
private manifest: WarmupManifest | null = null;
private lastFetchAtMs: number = 0;
private ttlMs: number = 300000;
private isFetching: boolean = false;
private manifestUrl: string = '';
private pending: ManifestReadyCallback[] = [];
/** 设置 manifest URL(environment 映射或显式覆盖的结果)。 */
public setManifestUrl(url: string): void {
this.manifestUrl = url;
}
/** 当前缓存的 manifest(可能为 null 或略旧)。 */
public getManifest(): WarmupManifest | null {
return this.manifest;
}
/** 缓存是否过期。 */
public isStale(): boolean {
return (Date.now() - this.lastFetchAtMs) > this.ttlMs;
}
/**
* 确保 manifest 可用且新鲜。
* - 无缓存:触发拉取,`onReady` 排队等 manifest 到达后回调(pending replay)。
* - 有缓存但过期:后台重拉(不阻塞),并在成功后回调 `onReady`(用于 version 变化通知)。
* - 有缓存且新鲜:立即以当前缓存回调 `onReady`。
*/
public ensureFresh(onReady?: ManifestReadyCallback): void {
if (this.manifest !== null) {
if (onReady !== undefined) {
onReady(this.manifest, false);
}
if (this.isStale()) {
this.fetch();
}
return;
}
// 无缓存:排队 + 拉取
if (onReady !== undefined) {
// 上限保护:超限直接丢弃新回调,避免弱网下 pending 无界增长。
if (this.pending.length < ManifestStore.MAX_PENDING) {
this.pending.push(onReady);
}
}
this.fetch();
}
/** 后台拉取 manifest(带 isFetching 保护,失败静默)。 */
private fetch(): void {
if (this.isFetching || this.manifestUrl.length === 0) {
return;
}
this.isFetching = true;
const request = http.createHttp();
request.request(
this.manifestUrl,
{
method: http.RequestMethod.GET,
header: { 'Accept': 'application/json' },
connectTimeout: 8000,
readTimeout: 8000
},
(err: Object | undefined, resp: http.HttpResponse): void => {
this.isFetching = false;
let success: boolean = false;
try {
if (err !== undefined && err !== null) {
return;
}
if (resp.responseCode !== 200) {
return;
}
this.ttlMs = ManifestStore.parseMaxAge(resp.header) ?? 300000;
const body: string = typeof resp.result === 'string'
? resp.result as string
: '';
if (body.length === 0) {
return;
}
const parsed: WarmupManifest = JSON.parse(body) as WarmupManifest;
if (parsed === null || parsed.hosts === undefined) {
return;
}
const oldVersion: string = this.manifest !== null ? this.manifest.version : '';
const versionChanged: boolean = oldVersion.length > 0 && oldVersion !== parsed.version;
this.manifest = parsed;
this.lastFetchAtMs = Date.now();
success = true;
// pending replay
const callbacks: ManifestReadyCallback[] = this.pending;
this.pending = [];
callbacks.forEach((cb: ManifestReadyCallback): void => {
try {
cb(parsed, false);
} catch (_e) {
// ignore
}
});
// version 变化通知(用于去重重置)
if (versionChanged) {
this.onVersionChanged(parsed);
}
} catch (_e) {
// 解析失败静默
} finally {
try {
request.destroy();
} catch (_e2) {
// ignore
}
// 拉取失败:清空 pending(符合"失败静默不回调"契约),避免回调无界累积。
// 下次 ensureFresh 会重新入队重试;去重状态未更新故不会误判已预热。
if (!success) {
this.pending = [];
}
}
}
);
}
/** version 变化钩子,由 Prewarmer 注入用于清理去重状态。 */
public onVersionChanged: (manifest: WarmupManifest) => void = (): void => {
// 默认空实现
};
/** 解析 `Cache-Control: max-age=NNN` → 毫秒;无则返回 null。 */
private static parseMaxAge(header: Object): number | null {
try {
const h: Record<string, ESObject> = header as Record<string, ESObject>;
let raw: ESObject = h['cache-control'];
if (raw === undefined) {
raw = h['Cache-Control'];
}
if (raw === undefined || typeof raw !== 'string') {
return null;
}
const cc: string = (raw as string).toLowerCase();
const idx: number = cc.indexOf('max-age=');
if (idx < 0) {
return null;
}
const rest: string = cc.substring(idx + 8);
const match: string = rest.split(',')[0].trim();
const seconds: number = parseInt(match, 10);
if (isNaN(seconds) || seconds <= 0) {
return null;
}
return seconds * 1000;
} catch (_e) {
return null;
}
}
}
|