100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
export interface FleetMessageSummary {
|
|
readonly id: string;
|
|
readonly direction: string;
|
|
readonly phoneNumber: string;
|
|
readonly content: string;
|
|
readonly timestamp: string;
|
|
}
|
|
|
|
export interface FleetMessagesSnapshot {
|
|
readonly latest?: FleetMessageSummary;
|
|
}
|
|
|
|
export interface FleetMessagesDataSource {
|
|
load(instanceId: string, signal?: AbortSignal): Promise<FleetMessagesSnapshot>;
|
|
}
|
|
|
|
export type FleetMessageLoadState = Readonly<{
|
|
latest?: FleetMessageSummary;
|
|
unavailable?: true;
|
|
}>;
|
|
|
|
const FLEET_MESSAGE_CONCURRENCY = 4;
|
|
export async function loadFleetMessageSummaries(
|
|
dataSource: FleetMessagesDataSource,
|
|
instanceIds: readonly string[],
|
|
signal: AbortSignal,
|
|
): Promise<ReadonlyMap<string, FleetMessageLoadState>> {
|
|
const results = new Map<string, FleetMessageLoadState>();
|
|
let cursor = 0;
|
|
async function worker(): Promise<void> {
|
|
while (!signal.aborted) {
|
|
const index = cursor++;
|
|
const instanceId = instanceIds[index];
|
|
if (instanceId === undefined) return;
|
|
try {
|
|
const result = await dataSource.load(instanceId, signal);
|
|
if (signal.aborted) return;
|
|
results.set(instanceId, result.latest ? { latest: result.latest } : {});
|
|
} catch {
|
|
if (signal.aborted) return;
|
|
results.set(instanceId, { unavailable: true });
|
|
}
|
|
}
|
|
}
|
|
await Promise.all(
|
|
Array.from({ length: Math.min(FLEET_MESSAGE_CONCURRENCY, instanceIds.length) }, () => worker()),
|
|
);
|
|
return signal.aborted ? new Map() : results;
|
|
}
|
|
|
|
function record(value: unknown): Record<string, unknown> | undefined {
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: undefined;
|
|
}
|
|
|
|
function parseMessage(value: unknown): FleetMessageSummary | undefined {
|
|
const item = record(value);
|
|
if (
|
|
typeof item?.id !== 'string' ||
|
|
typeof item.direction !== 'string' ||
|
|
typeof item.phoneNumber !== 'string' ||
|
|
typeof item.content !== 'string' ||
|
|
typeof item.timestamp !== 'string'
|
|
)
|
|
return undefined;
|
|
return {
|
|
id: item.id,
|
|
direction: item.direction,
|
|
phoneNumber: item.phoneNumber,
|
|
content: item.content,
|
|
timestamp: item.timestamp,
|
|
};
|
|
}
|
|
|
|
export function createFleetMessagesApiDataSource(
|
|
fetcher: typeof fetch = fetch,
|
|
): FleetMessagesDataSource {
|
|
return {
|
|
async load(instanceId, signal): Promise<FleetMessagesSnapshot> {
|
|
const response = await fetcher(
|
|
`/api/v1/instances/${encodeURIComponent(instanceId)}/messages?limit=1&offset=0`,
|
|
{
|
|
method: 'GET',
|
|
credentials: 'same-origin',
|
|
headers: { accept: 'application/json' },
|
|
...(signal ? { signal } : {}),
|
|
},
|
|
);
|
|
if (!response.ok) throw new Error(`Fleet messages request failed (${response.status}).`);
|
|
const root = record(await response.json());
|
|
if (!Array.isArray(root?.messages)) throw new Error('Fleet messages response is invalid.');
|
|
if (root.messages.length === 0) return {};
|
|
const latest = parseMessage(root.messages[0]);
|
|
if (!latest) throw new Error('Fleet messages response is invalid.');
|
|
return { latest };
|
|
},
|
|
};
|
|
}
|