feat(pets): generation RPCs, non-blocking gallery + gateway plumbing
- pet.generate / pet.hatch (parallel rows, off the reader thread) + cooperative pet.cancel; pet.export / pet.rename. - pet.gallery localOnly fast path + background manifest prefetch so the picker never blocks on petdex; rename follows the active-pet config. - gateway request gains optional timeout + AbortSignal for real Stop.
This commit is contained in:
parent
3faf768cde
commit
aab49f6927
5 changed files with 632 additions and 31 deletions
|
|
@ -217,29 +217,67 @@ export class JsonRpcGatewayClient {
|
|||
return () => this.stateHandlers.delete(handler)
|
||||
}
|
||||
|
||||
request<T>(method: string, params: Record<string, unknown> = {}, timeoutMs = this.options.requestTimeoutMs): Promise<T> {
|
||||
request<T>(
|
||||
method: string,
|
||||
params: Record<string, unknown> = {},
|
||||
timeoutMs = this.options.requestTimeoutMs,
|
||||
signal?: AbortSignal
|
||||
): Promise<T> {
|
||||
const socket = this.socket
|
||||
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return Promise.reject(new Error(this.options.notConnectedErrorMessage))
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
return Promise.reject(new DOMException('Aborted', 'AbortError'))
|
||||
}
|
||||
|
||||
const id = this.options.createRequestId(++this.nextId)
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let onAbort: (() => void) | undefined
|
||||
const detach = () => {
|
||||
if (onAbort && signal) {
|
||||
signal.removeEventListener('abort', onAbort)
|
||||
}
|
||||
}
|
||||
|
||||
const pending: PendingCall = {
|
||||
reject,
|
||||
resolve: value => resolve(value as T)
|
||||
resolve: value => {
|
||||
detach()
|
||||
resolve(value as T)
|
||||
},
|
||||
reject: error => {
|
||||
detach()
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (timeoutMs > 0) {
|
||||
pending.timer = setTimeout(() => {
|
||||
if (this.pending.delete(id)) {
|
||||
detach()
|
||||
reject(new Error(`request timed out: ${method}`))
|
||||
}
|
||||
}, timeoutMs)
|
||||
}
|
||||
|
||||
// Abort drops the pending call immediately (no dangling resolver/timer);
|
||||
// server-side cancellation is a separate cooperative RPC where it matters.
|
||||
if (signal) {
|
||||
onAbort = () => {
|
||||
const call = this.pending.get(id)
|
||||
if (call?.timer) {
|
||||
clearTimeout(call.timer)
|
||||
}
|
||||
this.pending.delete(id)
|
||||
detach()
|
||||
reject(new DOMException('Aborted', 'AbortError'))
|
||||
}
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
}
|
||||
|
||||
this.pending.set(id, pending)
|
||||
|
||||
try {
|
||||
|
|
@ -253,6 +291,7 @@ export class JsonRpcGatewayClient {
|
|||
)
|
||||
} catch (error) {
|
||||
this.clearPending(id)
|
||||
detach()
|
||||
reject(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue