fix(pi): retain ownership for uncertain mutations
This commit is contained in:
@@ -18,16 +18,24 @@ export type PiRpcResponse<T = unknown> = {
|
||||
export type PiRpcEvent = Record<string, unknown> & { type: string };
|
||||
export type PiRpcRetryPolicy = 'none' | 'read-only-once';
|
||||
|
||||
export type PiRpcLateResult =
|
||||
| { response: PiRpcResponse; error?: never }
|
||||
| { response?: never; error: PiProcessError };
|
||||
|
||||
export type PiRpcRequestOptions = {
|
||||
signal?: AbortSignal;
|
||||
timeoutMs?: number;
|
||||
retry?: PiRpcRetryPolicy;
|
||||
retainAfterTimeout?: boolean;
|
||||
onLateResult?(result: PiRpcLateResult): void;
|
||||
};
|
||||
|
||||
type PendingRequest = {
|
||||
commandType: string;
|
||||
timedOut: boolean;
|
||||
resolve(response: PiRpcResponse): void;
|
||||
reject(error: PiProcessError): void;
|
||||
notifyLate(result: PiRpcLateResult): void;
|
||||
cancel(): void;
|
||||
};
|
||||
|
||||
@@ -155,17 +163,30 @@ export class PiRpcClient {
|
||||
}
|
||||
pending.cancel();
|
||||
this.pending.delete(value.id);
|
||||
if (value.success) pending.resolve(value);
|
||||
else {
|
||||
pending.reject(new PiProcessError(
|
||||
if (value.success) {
|
||||
if (pending.timedOut) pending.notifyLate({ response: value });
|
||||
else pending.resolve(value);
|
||||
} else {
|
||||
const error = new PiProcessError(
|
||||
'PI_RPC_RESPONSE_ERROR',
|
||||
`Pi RPC ${value.command ?? pending.commandType} failed: ${value.error ?? 'unknown error'}`,
|
||||
{ generation: this.generation },
|
||||
));
|
||||
);
|
||||
if (pending.timedOut) pending.notifyLate({ error });
|
||||
else pending.reject(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.type === 'agent_settled') {
|
||||
for (const [id, pending] of this.pending) {
|
||||
if (!pending.timedOut) continue;
|
||||
pending.cancel();
|
||||
this.pending.delete(id);
|
||||
this.retire(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const listener of this.listeners) {
|
||||
try {
|
||||
listener(value as PiRpcEvent);
|
||||
@@ -185,7 +206,8 @@ export class PiRpcClient {
|
||||
for (const [id, pending] of this.pending) {
|
||||
pending.cancel();
|
||||
this.retire(id);
|
||||
pending.reject(error);
|
||||
if (pending.timedOut) pending.notifyLate({ error });
|
||||
else pending.reject(error);
|
||||
}
|
||||
this.pending.clear();
|
||||
}
|
||||
@@ -209,8 +231,12 @@ export class PiRpcClient {
|
||||
|
||||
const response = new Promise<PiRpcResponse<T>>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.pending.delete(id);
|
||||
this.retire(id);
|
||||
const pending = this.pending.get(id);
|
||||
if (options.retainAfterTimeout && pending) pending.timedOut = true;
|
||||
else {
|
||||
this.pending.delete(id);
|
||||
this.retire(id);
|
||||
}
|
||||
reject(new PiProcessError(
|
||||
'PI_RPC_TIMEOUT',
|
||||
`Pi RPC ${command.type} timed out after ${timeoutMs}ms`,
|
||||
@@ -218,18 +244,33 @@ export class PiRpcClient {
|
||||
));
|
||||
}, timeoutMs);
|
||||
const abort = (): void => {
|
||||
const pending = this.pending.get(id);
|
||||
this.pending.delete(id);
|
||||
this.retire(id);
|
||||
clearTimeout(timeout);
|
||||
reject(new PiProcessError('PI_RPC_ABORTED', `Pi RPC ${command.type} was aborted`, {
|
||||
const error = new PiProcessError('PI_RPC_ABORTED', `Pi RPC ${command.type} was aborted`, {
|
||||
generation: this.generation,
|
||||
}));
|
||||
});
|
||||
if (pending?.timedOut) pending.notifyLate({ error });
|
||||
else reject(error);
|
||||
};
|
||||
options.signal?.addEventListener('abort', abort, { once: true });
|
||||
this.pending.set(id, {
|
||||
commandType: command.type,
|
||||
timedOut: false,
|
||||
resolve: (value) => resolve(value as PiRpcResponse<T>),
|
||||
reject,
|
||||
notifyLate: (result) => {
|
||||
try {
|
||||
options.onLateResult?.(result);
|
||||
} catch (error) {
|
||||
try {
|
||||
this.onEventListenerError?.(error);
|
||||
} catch {
|
||||
// Diagnostics must not affect transport state.
|
||||
}
|
||||
}
|
||||
},
|
||||
cancel: () => {
|
||||
clearTimeout(timeout);
|
||||
options.signal?.removeEventListener('abort', abort);
|
||||
@@ -248,6 +289,12 @@ export class PiRpcClient {
|
||||
return record;
|
||||
} catch (error) {
|
||||
const pending = this.pending.get(id);
|
||||
if (error instanceof PiProcessError
|
||||
&& error.code === 'PI_RPC_TIMEOUT'
|
||||
&& options.retainAfterTimeout
|
||||
&& pending?.timedOut) {
|
||||
throw error;
|
||||
}
|
||||
if (pending) {
|
||||
pending.cancel();
|
||||
this.pending.delete(id);
|
||||
|
||||
Reference in New Issue
Block a user