Files
fangke/sdk/src/transport.ts
T
2026-09-15 16:07:18 +08:00

87 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { uuid } from "./util";
import { getVisitorId, getSessionId } from "./identity";
import type { CtkConfig } from "./config";
export type TransportConfig = Required<CtkConfig>;
// 批量传输:满 batchSize 或 flushMs 到期即发;4xx(除 429)不重试;
// 指数退避 + 抖动重试;重试耗尽按本批事件数计 dropped;queueMax 溢出丢最旧。
// 红线:任何异常不得冒泡到宿主页(定时器/事件回调内的异常无人捕获)。
export class Transport {
private q: object[] = [];
private timer: ReturnType<typeof setTimeout> | null = null;
private retry = 0;
dropped = 0;
constructor(public cfg: TransportConfig) {}
push(ev: object) {
if (this.q.length >= this.cfg.queueMax) {
this.q.shift();
this.dropped++;
}
this.q.push(ev);
if (this.q.length >= this.cfg.batchSize) this.flush();
else if (!this.timer)
this.timer = setTimeout(() => this.flush(), this.cfg.flushMs);
}
flush(beacon = false) {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (!this.q.length) return;
try {
const events = this.q;
this.q = [];
const body = JSON.stringify({
batch_id: uuid(),
site_id: this.cfg.siteId,
visitor_id: getVisitorId(),
session_id: getSessionId(),
sent_at: new Date().toISOString(),
// 由 index.ts(Task 15)注入;未注入时 undefined——JSON 序列化自然缺键,照发
context: (globalThis as { __ctk_ctx?: unknown }).__ctk_ctx,
events,
});
if (beacon && navigator.sendBeacon) {
navigator.sendBeacon(
this.cfg.endpoint,
new Blob([body], { type: "text/plain" })
);
return;
}
this.send(body, events.length);
} catch {
// 定时器/事件回调内的任何异常不得冒泡到宿主页
}
}
private async send(body: string, count: number) {
try {
const res = await fetch(this.cfg.endpoint, {
method: "POST",
keepalive: true,
headers: { "Content-Type": "text/plain" },
body,
});
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
this.retry = 0;
return; // 4xx 参数/权限错不重试(403 重试只会掩盖配置错误)
}
if (!res.ok) throw new Error(String(res.status));
this.retry = 0;
} catch {
if (this.retry++ < this.cfg.maxRetry) {
const delay = 500 * 2 ** this.retry * (0.8 + Math.random() * 0.4);
setTimeout(() => {
this.send(body, count).catch(() => {});
}, delay); // 重放同一 body 字符串——同 event_id 重放,服务端幂等去重
} else {
this.retry = 0;
this.dropped += count; // 按事件数计丢弃
}
}
}
}