消息信封、交付、恢复与错误
按类别和操作分发 v1 消息信封,容忍重复,并在支持回放时使用已存 History。
分发消息信封
每个实时消息信封先按 t 分发,再按 op 分发。处理程序必须幂等,因为历史回放可能返回已实时处理的事件。
消息信封基础类型
typescripttype EnvelopeBase<
TFamily extends 'tweet' | 'account' | 'charity' | 'control' | 'news',
TOp extends string,
TPayload extends object,
> = {
v: 1;
t: TFamily;
op: TOp;
id?: string;
ts: number;
d: TPayload;
};分发操作
TweetStream 发送 18 种服务端操作。先按 t、再按 op 分发每个消息信封;遇到未知操作时忽略该消息,不要关闭连接。客户端发送两种命令:auth_ping 用于认证检查,twitter_handles 用于在 handle-management 连接上添加或移除账号。
| 类别 | 操作 | 含义 |
|---|---|---|
tweet | content | 帖子内容 |
tweet | meta | 帖子富化 |
tweet | update | 帖子更新 |
tweet | delete | 帖子删除 |
tweet | pin | 帖子置顶 |
tweet | unpin | 帖子取消置顶 |
account | profile_update | 资料更新 |
account | follow | 关注账号 |
account | unfollow | 取消关注账号 |
account | affiliate_update | 关联账号变更 |
account | callout | 代币喊单 |
account | callout_update | 喊单富化 |
account | thesis | FOMO 观点 |
account | token_verified | 观察到 FOMO 代币认证 |
charity | added | 新增慈善项目 |
news | article | 新闻文章 |
control | auth_pong | 认证 pong |
control | twitter_handles_result | 账号管理命令结果 |
按事件流处理交付
消息信封没有确认、恢复 token 或序列号。不要根据 ts 或 id 推断顺序或恰好一次交付。
处理程序必须幂等,并保留足够的本地状态,以识别应用已经处理的事件。
重连后回补
固定 startDate 和 endDate,在其他筛选条件不变的情况下持续使用 nextCursor,直到返回 null。以幂等方式处理每行。如果中断后的回放需要继续而不是重启,请一起保存时间范围和 cursor。
History 返回已存事件,不保证完整性或保留期限。AFFILIATE 返回已存的关联账号变更,不是当前列表快照。delete、pin 和 unpin 仍只在实时流中发送。
回放恢复窗口
typescripttype HistoryRow = {
time: string;
tweetId: string;
};
type HistoryPage = {
data: Array<HistoryRow>;
metadata: { nextCursor: string | null };
};
async function replayAfterReconnect(
checkpoint: string,
processEvent: (event: HistoryRow) => Promise<void>,
saveCheckpoint: (eventTime: string) => Promise<void>,
) {
const apiKey = process.env.TWEETSTREAM_API_KEY;
if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY");
const url = new URL("https://api.tweetstream.io/api/history");
const replayEnd = new Date().toISOString();
url.searchParams.set("handles", "marketdesk");
url.searchParams.set("startDate", checkpoint);
url.searchParams.set("endDate", replayEnd);
url.searchParams.set("limit", "1000");
url.searchParams.set("type", "TWEET");
const pages: Array<Array<HistoryRow>> = [];
let cursor: string | null = null;
do {
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`Replay failed (${response.status}): ${detail}`);
}
const page = (await response.json()) as HistoryPage;
pages.push(page.data);
cursor = page.metadata.nextCursor;
} while (cursor);
for (const event of pages.reverse().flatMap((page) => [...page].reverse())) {
await processEvent(event);
}
await saveCheckpoint(replayEnd);
}修复握手错误
WebSocket 连接升级失败时,会在连接被接受前返回 JSON。客户端可以安全记录该响应。
| 状态 | 可能原因 | 修复方式 |
|---|---|---|
400 | WebSocket 协议 header 缺失或无效 | 发送 tweetstream.v1 和有效认证 token 协议 |
401 | API 密钥缺失、为空或无效 | 在控制台重新生成或轮换 API 密钥 |
403 | 订阅状态不允许实时流 | 开始试用、续费或升级账号 |
429 | 活跃 WebSocket 连接数达到限制 | 关闭旧连接或升级套餐 |
503 | 连接限制服务暂时不可用 | 使用退避策略重试 |
读取连接升级拒绝响应
json{
"error": "WebSocket connection limit reached (3/3 active). Close an existing connection and retry.",
"status": 429
}