feat(api): 访问日志与 HTTP/运行时指标、安全响应头与面板 CSP、跨站写拦截、请求体读截止、SSE 定期重鉴权与续传、404/405/413 信封、status 按所有权裁剪、停机并发排空
This commit is contained in:
24 files changed
+1453
-55
No files matched your search
@@ -18,6 +18,7 @@ class FakeEventSource implements EventSourceLike {
|
||||
readyState = 0; // CONNECTING
|
||||
closed = false;
|
||||
readonly url: string;
|
||||
private readonly listeners = new Map<string, Array<(ev: unknown) => void>>();
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
@@ -28,6 +29,13 @@ class FakeEventSource implements EventSourceLike {
|
||||
this.readyState = 2; // CLOSED
|
||||
}
|
||||
|
||||
addEventListener(type: string, listener: (ev: unknown) => void): void {
|
||||
this.listeners.set(type, [...(this.listeners.get(type) ?? []), listener]);
|
||||
}
|
||||
emitEvent(type: string): void {
|
||||
for (const fn of this.listeners.get(type) ?? []) fn({ data: "" });
|
||||
}
|
||||
|
||||
// Test drivers mirroring what the browser would invoke.
|
||||
emitOpen(): void {
|
||||
this.readyState = 1; // OPEN
|
||||
@@ -181,4 +189,18 @@ describe("LogStreamController", () => {
|
||||
expect(ctrl.getSnapshot().lines).toHaveLength(0);
|
||||
expect(ctrl.getSnapshot().status).toBe("open");
|
||||
});
|
||||
|
||||
it("ends for good when the server revokes the stream", () => {
|
||||
const { factory, created } = makeFactory();
|
||||
const ctrl = new LogStreamController({ url: "/api/v1/servers/s/console", factory });
|
||||
ctrl.open();
|
||||
const es = created[0];
|
||||
es.emitOpen();
|
||||
es.emitMessage("[12:00:00] [Server thread/INFO]: hello");
|
||||
es.emitEvent("revoked");
|
||||
expect(ctrl.getSnapshot().status).toBe("ended");
|
||||
expect(es.closed).toBe(true);
|
||||
expect(ctrl.getSnapshot().lines).toHaveLength(1);
|
||||
expect(created).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -59,6 +59,7 @@ export interface EventSourceLike {
|
||||
onerror: ((ev: any) => void) | null;
|
||||
readyState: number;
|
||||
close(): void;
|
||||
addEventListener(type: string, listener: (ev: any) => void): void;
|
||||
}
|
||||
|
||||
/** Builds an EventSource for a URL (production: the browser ctor; tests: a fake). */
|
||||
@@ -175,6 +176,14 @@ export class LogStreamController {
|
||||
this.es = es;
|
||||
es.onopen = () => this.setStatus("open");
|
||||
es.onmessage = (ev) => this.pushLine(ev.data);
|
||||
// The server re-checks a running stream's grant (session still live, caller
|
||||
// still owns the server) and sends "revoked" before it closes. That close is
|
||||
// final: an auto-retry would only be refused again.
|
||||
es.addEventListener("revoked", () => {
|
||||
if (this.es !== es) return;
|
||||
this.disconnect();
|
||||
this.setStatus("ended");
|
||||
});
|
||||
es.onerror = () => {
|
||||
// Native EventSource auto-reconnects on a transient drop (readyState
|
||||
// returns to CONNECTING); only a fatal response — non-200 / wrong
|
||||
|
||||
Reference in new issue
Block a user