Files
multi-simadmin/apps/web/src/fleet/fleet-messages-api-data-source.ts
T

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 };
},
};
}