mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix(sdk): reject work after client close
This commit is contained in:
parent
1385da8d3f
commit
8d596aa651
4 changed files with 137 additions and 6 deletions
|
|
@ -327,8 +327,10 @@ export class OpenClaw {
|
|||
});
|
||||
private readonly replayByRunId = new Map<string, OpenClawEvent[]>();
|
||||
private connected = false;
|
||||
private closed = false;
|
||||
private eventPumpPromise: Promise<void> | null = null;
|
||||
private eventPumpReady: Promise<void> | null = null;
|
||||
private closePromise: Promise<void> | null = null;
|
||||
|
||||
constructor(options: OpenClawOptions = {}) {
|
||||
this.transport =
|
||||
|
|
@ -351,24 +353,45 @@ export class OpenClaw {
|
|||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
this.assertOpen();
|
||||
if (this.connected) {
|
||||
await this.startEventPump();
|
||||
this.assertOpen();
|
||||
return;
|
||||
}
|
||||
if (isConnectableTransport(this.transport)) {
|
||||
await this.transport.connect();
|
||||
}
|
||||
this.assertOpen();
|
||||
this.connected = true;
|
||||
await this.startEventPump();
|
||||
this.assertOpen();
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.transport.close?.();
|
||||
await this.eventPumpPromise?.catch(() => {});
|
||||
this.normalizedEvents.close();
|
||||
this.eventPumpPromise = null;
|
||||
this.eventPumpReady = null;
|
||||
this.connected = false;
|
||||
if (this.closePromise) {
|
||||
return await this.closePromise;
|
||||
}
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
this.closePromise = (async () => {
|
||||
try {
|
||||
await this.transport.close?.();
|
||||
await this.eventPumpPromise?.catch(() => {});
|
||||
} finally {
|
||||
this.normalizedEvents.close();
|
||||
this.eventPumpPromise = null;
|
||||
this.eventPumpReady = null;
|
||||
this.connected = false;
|
||||
}
|
||||
})();
|
||||
try {
|
||||
await this.closePromise;
|
||||
} finally {
|
||||
this.closePromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
async request<T = unknown>(
|
||||
|
|
@ -377,6 +400,7 @@ export class OpenClaw {
|
|||
options?: GatewayRequestOptions,
|
||||
): Promise<T> {
|
||||
await this.connect();
|
||||
this.assertOpen();
|
||||
return await this.transport.request<T>(method, params, options);
|
||||
}
|
||||
|
||||
|
|
@ -392,13 +416,21 @@ export class OpenClaw {
|
|||
}
|
||||
|
||||
rawEvents(filter?: (event: GatewayEvent) => boolean): AsyncIterable<GatewayEvent> {
|
||||
this.assertOpen();
|
||||
return this.transport.events(filter);
|
||||
}
|
||||
|
||||
private assertOpen(): void {
|
||||
if (this.closed) {
|
||||
throw new Error("OpenClaw SDK client is closed");
|
||||
}
|
||||
}
|
||||
|
||||
private async *iterateEvents(
|
||||
filter?: (event: OpenClawEvent) => boolean,
|
||||
): AsyncIterable<OpenClawEvent> {
|
||||
await this.connect();
|
||||
this.assertOpen();
|
||||
for await (const event of this.normalizedEvents.stream(filter)) {
|
||||
yield event;
|
||||
}
|
||||
|
|
@ -409,6 +441,7 @@ export class OpenClaw {
|
|||
filter?: (event: OpenClawEvent) => boolean,
|
||||
): AsyncIterable<OpenClawEvent> {
|
||||
await this.connect();
|
||||
this.assertOpen();
|
||||
const replayEvents = this.replaySnapshot(runId);
|
||||
let hasCanonicalAssistantRunEvent = replayEvents.some(isAssistantRunEvent);
|
||||
let hasTerminalRunEvent = replayEvents.some(isTerminalRunEvent);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,51 @@ class FakeTransport implements OpenClawTransport {
|
|||
}
|
||||
}
|
||||
|
||||
class DelayedConnectTransport extends FakeTransport {
|
||||
connectCalls = 0;
|
||||
private finishConnectCurrent: (() => void) | null = null;
|
||||
|
||||
async connect(): Promise<void> {
|
||||
this.connectCalls += 1;
|
||||
await new Promise<void>((resolve) => {
|
||||
this.finishConnectCurrent = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
finishConnect(): void {
|
||||
const finish = this.finishConnectCurrent;
|
||||
if (!finish) {
|
||||
throw new Error("expected pending connect");
|
||||
}
|
||||
this.finishConnectCurrent = null;
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
class ClosingEventPumpTransport extends FakeTransport {
|
||||
onFirstEventPoll?: () => void;
|
||||
|
||||
override events(): AsyncIterable<GatewayEvent> {
|
||||
return {
|
||||
[Symbol.asyncIterator]: (): AsyncIterator<GatewayEvent> => {
|
||||
let firstPoll = true;
|
||||
return {
|
||||
next: async (): Promise<IteratorResult<GatewayEvent>> => {
|
||||
if (firstPoll) {
|
||||
firstPoll = false;
|
||||
this.onFirstEventPoll?.();
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
return { done: true, value: undefined as never };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class EventsOnlyTransport implements OpenClawTransport {
|
||||
constructor(private readonly eventSource: AsyncIterable<GatewayEvent>) {}
|
||||
|
||||
|
|
@ -631,6 +676,42 @@ describe("OpenClaw SDK", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("keeps close terminal when it races a pending connect", async () => {
|
||||
const transport = new DelayedConnectTransport({
|
||||
"agents.list": { agents: [] },
|
||||
});
|
||||
const oc = new OpenClaw({ transport });
|
||||
|
||||
const connect = oc.connect();
|
||||
const close = oc.close();
|
||||
transport.finishConnect();
|
||||
|
||||
await expect(connect).rejects.toThrow("OpenClaw SDK client is closed");
|
||||
await close;
|
||||
await expect(oc.agents.list()).rejects.toThrow("OpenClaw SDK client is closed");
|
||||
await expect(oc.events()[Symbol.asyncIterator]().next()).rejects.toThrow(
|
||||
"OpenClaw SDK client is closed",
|
||||
);
|
||||
expect(() => oc.rawEvents()).toThrow("OpenClaw SDK client is closed");
|
||||
expect(transport.connectCalls).toBe(1);
|
||||
expect(transport.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not request after close races event pump startup", async () => {
|
||||
const transport = new ClosingEventPumpTransport({
|
||||
"agents.list": { agents: [] },
|
||||
});
|
||||
const oc = new OpenClaw({ transport });
|
||||
let closePromise: Promise<void> | undefined;
|
||||
transport.onFirstEventPoll = () => {
|
||||
closePromise = oc.close();
|
||||
};
|
||||
|
||||
await expect(oc.agents.list()).rejects.toThrow("OpenClaw SDK client is closed");
|
||||
await closePromise;
|
||||
expect(transport.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it("cancels runs and checks model auth status through current Gateway methods", async () => {
|
||||
const transport = new FakeTransport({
|
||||
agent: { status: "accepted", runId: "run_without_session" },
|
||||
|
|
|
|||
|
|
@ -50,4 +50,13 @@ describe("GatewayClientTransport", () => {
|
|||
await connectExpectation;
|
||||
expect(client?.stopAndWait).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("rejects reconnect attempts after close", async () => {
|
||||
const transport = new GatewayClientTransport();
|
||||
|
||||
await transport.close();
|
||||
|
||||
await expect(transport.connect()).rejects.toThrow("gateway transport is closed");
|
||||
expect(gatewayClientMocks.instances).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -80,12 +80,16 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport {
|
|||
private connectPromise: Promise<void> | null = null;
|
||||
private rejectPendingConnect: ((error: Error) => void) | null = null;
|
||||
private closePromise: Promise<void> | null = null;
|
||||
private closed = false;
|
||||
|
||||
constructor(options: GatewayClientTransportOptions = {}) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
connect(): Promise<void> {
|
||||
if (this.closed) {
|
||||
return Promise.reject(new Error("gateway transport is closed"));
|
||||
}
|
||||
if (this.connectPromise) {
|
||||
return this.connectPromise;
|
||||
}
|
||||
|
|
@ -146,6 +150,10 @@ export class GatewayClientTransport implements ConnectableOpenClawTransport {
|
|||
if (this.closePromise) {
|
||||
return await this.closePromise;
|
||||
}
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
this.eventsHub.close();
|
||||
const client = this.client;
|
||||
this.client = null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue