Updated Wrapper component, added queue for all ttp requests, updated some desktop app configuration,

This commit is contained in:
Alois 2026-04-06 01:41:51 +02:00
commit 0903f96ba3
10 changed files with 207 additions and 106 deletions

View file

@ -69,20 +69,13 @@ export type SchemaMap = Record<
type SendOptions = {
id?: number;
noResponse?: boolean;
};
export type BoundSendFn<T extends SchemaMap> = {
<K extends keyof T & string>(
type: K,
data: z.input<T[K]["request"]>,
options: { id?: number; noResponse: true },
): Promise<void>;
<K extends keyof T & string>(
type: K,
data: z.input<T[K]["request"]>,
options?: { id?: number; noResponse?: false },
options?: { id?: number },
): Promise<TypedMessage<z.output<T[K]["response"]>>>;
};
@ -125,6 +118,7 @@ export function createTransportClient<T extends SchemaMap>(
let currentConnection: ActiveConnection | null = null;
let currentReadyState: number = READY_STATE.CLOSED;
let nextRequestId = 1;
let sendQueueTail: Promise<void> = Promise.resolve();
let configuredUrl = options.url;
/**
@ -192,6 +186,20 @@ export function createTransportClient<T extends SchemaMap>(
notifyClosed(connection, error);
};
/**
* Serializes outbound send work so only one request is active at a time.
* @param task Request task to run in queue order.
* @returns Promise for the task result.
*/
const enqueueSend = <T>(task: () => Promise<T>) => {
const queuedTask = sendQueueTail.then(task, task);
sendQueueTail = queuedTask.then(
() => undefined,
() => undefined,
);
return queuedTask;
};
/**
* Handles decoded incoming messages and resolves request promises or push listeners.
* @param message Decoded incoming message.
@ -515,8 +523,8 @@ export function createTransportClient<T extends SchemaMap>(
* Sends a typed protocol request over the current connection.
* @param type Protocol message type.
* @param input Optional request payload.
* @param options Optional id and response behavior.
* @returns Promise for response message or void when no response is expected.
* @param options Optional request id.
* @returns Promise for the typed response message.
*/
const send: BoundSendFn<T> = ((
type: string,
@ -527,8 +535,6 @@ export function createTransportClient<T extends SchemaMap>(
return Promise.reject(new Error("Transport is not connected"));
}
const connection = currentConnection;
try {
const schema = schemas[type];
let payload: Record<string, unknown>;
@ -555,69 +561,66 @@ export function createTransportClient<T extends SchemaMap>(
payload = coercePayload(input ?? {});
}
if (!options?.id && !options?.noResponse) {
const array = new Uint32Array(1);
crypto.getRandomValues(array);
options = options ?? {};
options.id = array[0];
}
const requestOptions = options ? { ...options } : {};
const enqueuedConnection = currentConnection;
if (type !== "ping") {
log(2, "ttp", "gray", "Sent:", type, payload, { id: options.id });
}
return enqueueSend(() => {
if (
!enqueuedConnection ||
currentConnection !== enqueuedConnection ||
currentReadyState !== READY_STATE.OPEN ||
enqueuedConnection.closeNotified
) {
return Promise.reject(new Error("Transport is not connected"));
}
const expectsResponse = !options?.noResponse;
const requestId = resolveRequestId(
options.id,
expectsResponse,
pending,
() => {
const current = nextRequestId;
nextRequestId = current >= MAX_REQUEST_ID ? 1 : current + 1;
return current;
},
);
const messageBytes = encodeCommunicationMessage({
id: requestId,
type,
data: payload,
});
if (!expectsResponse) {
return writeMessageOnPersistentStream(connection, messageBytes).catch(
(error) => {
handleConnectionFailure(connection, error);
throw error;
const requestId = resolveRequestId(
requestOptions.id,
true,
pending,
() => {
const current = nextRequestId;
nextRequestId = current >= MAX_REQUEST_ID ? 1 : current + 1;
return current;
},
);
}
return new Promise<TypedMessage>((resolve, reject) => {
const timeoutId = setTimeout(() => {
pending.delete(requestId);
reject(
new Error(
`Request "${type}" timed out after ${RESPONSE_TIMEOUT}ms`,
),
);
}, RESPONSE_TIMEOUT);
if (type !== "ping") {
log(2, "ttp", "gray", "Sent:", type, payload, { id: requestId });
}
pending.set(requestId, {
requestType: type,
resolve,
reject,
timeoutId,
const messageBytes = encodeCommunicationMessage({
id: requestId,
type,
data: payload,
});
void writeMessageOnPersistentStream(connection, messageBytes).catch(
(error) => {
handleConnectionFailure(connection, error);
clearTimeout(timeoutId);
return new Promise<TypedMessage>((resolve, reject) => {
const timeoutId = setTimeout(() => {
pending.delete(requestId);
reject(error);
},
);
reject(
new Error(
`Request "${type}" timed out after ${RESPONSE_TIMEOUT}ms`,
),
);
}, RESPONSE_TIMEOUT);
pending.set(requestId, {
requestType: type,
resolve,
reject,
timeoutId,
});
void writeMessageOnPersistentStream(enqueuedConnection, messageBytes).catch(
(error) => {
handleConnectionFailure(enqueuedConnection, error);
clearTimeout(timeoutId);
pending.delete(requestId);
reject(error);
},
);
});
});
} catch (error) {
return Promise.reject(error);