Skip to content

Commit

Permalink
feat!: Client#post() always returns array of responses (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
nzakas authored Dec 30, 2024
1 parent 18a60a3 commit 976b500
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 63 deletions.
51 changes: 38 additions & 13 deletions src/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
* @author Nicholas C. Zakas
*/

/* eslint-disable no-console */

//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
Expand All @@ -19,15 +17,37 @@ import {
BlueskyStrategy,
} from "./index.js";

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/** @typedef {import("./client.js").SuccessResponse} SuccessResponse */

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

/**
* Determines if a response is successful.
* @param {any} response The response to check.
* @returns {response is SuccessResponse} True if the response is successful, false if not.
*/
function isSuccessResponse(response) {
return response.ok;
}

//-----------------------------------------------------------------------------
// Parse CLI Arguments
//-----------------------------------------------------------------------------

// appease TypeScript
const booleanType = /** @type {const} */ ("boolean");

const options = {
twitter: { type: "boolean", short: "t" },
mastodon: { type: "boolean", short: "m" },
bluesky: { type: "boolean", short: "b" },
help: { type: "boolean", short: "h" },
twitter: { type: booleanType, short: "t" },
mastodon: { type: booleanType, short: "m" },
bluesky: { type: booleanType, short: "b" },
help: { type: booleanType, short: "h" },
};

const { values: flags, positionals } = parseArgs({
Expand Down Expand Up @@ -69,6 +89,7 @@ const env = new Env();
// Determine which strategies to use
//-----------------------------------------------------------------------------

/** @type {Array<TwitterStrategy|MastodonStrategy|BlueskyStrategy>} */
const strategies = [];

if (flags.twitter) {
Expand Down Expand Up @@ -106,10 +127,14 @@ if (flags.bluesky) {
//-----------------------------------------------------------------------------

const client = new Client({ strategies });
const response = await client.post(message);

for (const [service, result] of Object.entries(response)) {
console.log(`${service} result`);
console.log(JSON.stringify(result, null, 2));
console.log("");
}
const responses = await client.post(message);

responses.forEach((response, index) => {
if (isSuccessResponse(response)) {
console.log(`✅ ${strategies[index].name} succeeded.`);
console.log(response.response);
} else {
console.log(`❌ ${strategies[index].name} failed.`);
console.error(response.reason);
}
});
102 changes: 68 additions & 34 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,62 @@
* @property {(message: string) => Promise<any>} post A function that posts a message.
*/

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

/**
* Represents a successful response.
*/
export class SuccessResponse {
/**
* Indicates success.
* @type {boolean}
* @const
*/
ok = true;

/**
* The message posted.
* @type {Object}
*/
response;

/**
* Creates a new instance.
* @param {Object} response The response.
*/
constructor(response) {
this.response = response;
}
}

/**
* Represents a failure response.
*/
export class FailureResponse {
/**
* Indicates failure.
* @type {boolean}
* @const
*/
ok = false;

/**
* The error or response.
* @type {Object}
*/
reason;

/**
* Creates a new instance.
* @param {Object} reason The reason for failure.
*/
constructor(reason) {
this.reason = reason;
}
}

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -52,41 +108,19 @@ export class Client {
/**
* Posts a message using all strategies.
* @param {string} message The message to post.
* @returns {Promise<Object>} A promise that resolves with an array of results.
* @returns {Promise<Array<SuccessResponse|FailureResponse>>} A promise that resolves with an array of results.
*/
async post(message) {
const results = await Promise.allSettled(
this.#strategies.map(strategy => strategy.post(message)),
);

// find any failed results
/** @type {Array<number>} */
const failedIndices = [];
const failed = /** @type {Array<PromiseRejectedResult>} */ (
results.filter((result, i) => {
if (result.status === "rejected") {
failedIndices.push(i);
return true;
}

return false;
})
);

// if there are failed results, throw an error with the failing strategy names
if (failed.length) {
throw new AggregateError(
failed.map(result => result.reason),
`Failed to post to strategies: ${failedIndices.map(i => this.#strategies[i].name).join(", ")}`,
);
}

// otherwise return the response payloads keyed by strategy name
return Object.fromEntries(
results.map((result, i) => [
this.#strategies[i].name,
/** @type {PromiseFulfilledResult<Object>} */ (result).value,
]),
);
return (
await Promise.allSettled(
this.#strategies.map(strategy => strategy.post(message)),
)
).map(result => {
if (result.status === "fulfilled") {
return new SuccessResponse(result.value);
} else {
return new FailureResponse(result.reason);
}
});
}
}
32 changes: 17 additions & 15 deletions tests/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//-----------------------------------------------------------------------------

import { strict as assert } from "node:assert";
import { Client } from "../src/client.js";
import { Client, SuccessResponse, FailureResponse } from "../src/client.js";

//-----------------------------------------------------------------------------
// Tests
Expand Down Expand Up @@ -62,13 +62,13 @@ describe("Client", function () {
const client = new Client({ strategies });
const response = await client.post("Hello, world!");

assert.deepStrictEqual(response, {
test1: "test1",
test2: "test2",
});
assert.deepStrictEqual(response, [
new SuccessResponse("test1"),
new SuccessResponse("test2"),
]);
});

it("should throw an error when one strategy fails", async function () {
it("should return failure response one strategy fails", async function () {
const strategies = [
{
name: "test1",
Expand All @@ -85,14 +85,15 @@ describe("Client", function () {
];

const client = new Client({ strategies });
const results = await client.post("Hello, world!");

await assert.rejects(client.post("Hello, world!"), {
name: "AggregateError",
message: "Failed to post to strategies: test2",
});
assert.deepStrictEqual(results, [
new SuccessResponse("test1"),
new FailureResponse(new Error("test2")),
]);
});

it("should throw an error when multiple strategies fail", async function () {
it("should return failure responses when all strategies fail", async function () {
const strategies = [
{
name: "test1",
Expand All @@ -109,11 +110,12 @@ describe("Client", function () {
];

const client = new Client({ strategies });
const results = await client.post("Hello, world!");

await assert.rejects(client.post("Hello, world!"), {
name: "AggregateError",
message: "Failed to post to strategies: test1, test2",
});
assert.deepStrictEqual(results, [
new FailureResponse(new Error("test1")),
new FailureResponse(new Error("test2")),
]);
});
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"files": ["src/index.js"],
"files": ["src/index.js", "src/bin.js"],
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
Expand Down

0 comments on commit 976b500

Please sign in to comment.