Skip to content

Commit

Permalink
Begin work on requestMany
Browse files Browse the repository at this point in the history
  • Loading branch information
htunnicliff committed Oct 21, 2023
1 parent 3a472ea commit 233e942
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 2 deletions.
36 changes: 36 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,39 @@ export function getErrorFromInvocation<T extends Invocation>(

return null;
}

/**
* Note: This could be more defensive, but for now I'm willing to trust that JMAP
* servers will follow the specs (meaning: each method call will have a response
* with a matching ID, no duplicates, etc. if the status code is 2xx)
*/
export function getResultsForMethodCalls(
methodCallResponses: Array<Invocation<any>>,
{ returnErrors }: { returnErrors: boolean }
) {
return Object.fromEntries(
methodCallResponses.map(([name, data, id]) => {
if (!returnErrors) {
return [id, data];
}

if (name === "error") {
return [
id,
{
data: null,
error: data as ProblemDetails,
},
];
} else {
return [
id,
{
data,
error: null,
},
];
}
})
);
}
103 changes: 101 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import {
expandURITemplate,
getErrorFromInvocation,
getResultsForMethodCalls,
isErrorInvocation,
} from "./helpers";
import type { Requests, Responses } from "./types/client";
Expand Down Expand Up @@ -89,6 +90,103 @@ export function createClient<Config extends ClientConfig>({
cache: "no-cache",
}).then((res) => res.json());

async function requestMany(
requests: Record<string, any>,
options: {
fetchInit?: RequestInit;
using?: JMAPRequest["using"];
createdIds?: JMAPRequest["createdIds"];
} = {}
) {
// Extract options
const { using = [], fetchInit, createdIds: createdIdsInput } = options;

// Assemble method calls
const methodNames = new Set<string>();
const methodCalls = Object.entries(requests).map(([id, [name, args]]) => {
methodNames.add(name);
return [name, args, id] as Invocation<typeof args>;
});

// Build request
const body: JMAPRequest<typeof methodCalls> = {
using: [
...getCapabilitiesForMethodCalls({
methodNames,
availableCapabilities: capabilities,
}),
...using,
],
methodCalls,
createdIds: createdIdsInput,
};

// Ensure session is loaded (if not already)
const { apiUrl } = await session;

// Send request
const response = await fetch(apiUrl, {
method: "POST",
headers: {
Authorization: authHeader,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(body),
...fetchInit,
});

// Handle 4xx-5xx errors
if (!response.ok) {
let error: string | ProblemDetails;

if (response.headers.get("Content-Type")?.includes("json")) {
error = (await response.json()) as ProblemDetails;
} else {
error = await response.text();
}

throw error;
}

// Handle success
const { methodResponses, sessionState, createdIds } =
(await response.json()) as JMAPResponse;

const meta = {
sessionState,
createdIds,
response,
};

switch (errorHandling) {
case "throw": {
const errors = methodResponses
.map(getErrorFromInvocation)
.filter((e): e is NonNullable<typeof e> => e !== null);

if (errors.length > 0) {
throw errors;
} else {
return [
getResultsForMethodCalls(methodResponses, {
returnErrors: false,
}),
meta,
];
}
}
case "return": {
return [
getResultsForMethodCalls(methodResponses, {
returnErrors: true,
}),
meta,
];
}
}
}

/**
* Send a JMAP request containing a single method call
*/
Expand Down Expand Up @@ -131,7 +229,7 @@ export function createClient<Config extends ClientConfig>({
const invocation: Invocation<Args> = [method, args, "r1"];

// Build request
const body = {
const body: JMAPRequest<[Invocation<Args>]> = {
using: [
...getCapabilitiesForMethodCalls({
methodNames: [method],
Expand All @@ -141,7 +239,7 @@ export function createClient<Config extends ClientConfig>({
],
methodCalls: [invocation],
createdIds: createdIdsInput,
} satisfies JMAPRequest<[Invocation<Args>]>;
};

// Ensure session is loaded (if not already)
const { apiUrl } = await session;
Expand Down Expand Up @@ -332,6 +430,7 @@ export function createClient<Config extends ClientConfig>({
session,
getPrimaryAccount,
request,
requestMany,
uploadBlob,
downloadBlob,
connectEventSource,
Expand Down

0 comments on commit 233e942

Please sign in to comment.