Skip to content

Commit 8cc82f4

Browse files
committed
chore: Refactor Mastodon specific logic with hook API
1 parent 16084f7 commit 8cc82f4

File tree

11 files changed

+223
-64
lines changed

11 files changed

+223
-64
lines changed

src/adapters/action/dispatcher-http-hook-mastodon.spec.ts renamed to src/adapters/action/dispatcher-http.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { httpGet, HttpMockImpl, httpPost } from "../../__mocks__";
22
import { MastoHttpError, MastoTimeoutError } from "../errors";
3+
import { ActionDispatcherHookMastodon } from "../hook/hook-action-dispatcher-mastodon";
34
import { HttpActionDispatcher } from "./dispatcher-http";
4-
import { HttpActionDispatcherHookMastodon } from "./dispatcher-http-hook-mastodon";
55

6-
describe("DispatcherHttp", () => {
6+
describe("HttpActionDispatcher", () => {
77
afterEach(() => {
88
httpGet.mockClear();
99
httpPost.mockClear();
@@ -13,7 +13,7 @@ describe("DispatcherHttp", () => {
1313
const http = new HttpMockImpl();
1414
const dispatcher = new HttpActionDispatcher(
1515
http,
16-
new HttpActionDispatcherHookMastodon(http),
16+
new ActionDispatcherHookMastodon(http),
1717
);
1818

1919
httpPost.mockResolvedValueOnce({ id: "1" });
@@ -43,7 +43,7 @@ describe("DispatcherHttp", () => {
4343
const http = new HttpMockImpl();
4444
const dispatcher = new HttpActionDispatcher(
4545
http,
46-
new HttpActionDispatcherHookMastodon(http, 1),
46+
new ActionDispatcherHookMastodon(http, 1),
4747
);
4848

4949
httpPost.mockResolvedValueOnce({ id: "1" });
@@ -65,7 +65,7 @@ describe("DispatcherHttp", () => {
6565
const http = new HttpMockImpl();
6666
const dispatcher = new HttpActionDispatcher(
6767
http,
68-
new HttpActionDispatcherHookMastodon(http),
68+
new ActionDispatcherHookMastodon(http),
6969
);
7070

7171
httpPost.mockResolvedValueOnce({ id: "1" });
@@ -85,7 +85,7 @@ describe("DispatcherHttp", () => {
8585
const http = new HttpMockImpl();
8686
const dispatcher = new HttpActionDispatcher(
8787
http,
88-
new HttpActionDispatcherHookMastodon(http),
88+
new ActionDispatcherHookMastodon(http),
8989
);
9090

9191
httpPost.mockResolvedValueOnce({ id: "1" });
Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,23 @@
11
import {
2-
type Action,
32
type ActionDispatcher,
43
type ActionDispatcherHook,
4+
type AnyAction,
55
type Http,
66
} from "../../interfaces";
77
import { PaginatorHttp } from "./paginator-http";
88

9-
export type HttpActionType = "fetch" | "create" | "update" | "remove" | "list";
10-
export type HttpAction = Action<HttpActionType>;
11-
12-
export class HttpActionDispatcher implements ActionDispatcher<HttpAction> {
9+
export class HttpActionDispatcher implements ActionDispatcher<AnyAction> {
1310
constructor(
1411
private readonly http: Http,
15-
private readonly hook: ActionDispatcherHook<HttpAction>,
12+
private readonly hook?: ActionDispatcherHook,
1613
) {}
1714

18-
dispatch<T>(action: HttpAction): T | Promise<T> {
19-
if (this.hook != undefined) {
20-
action = this.hook.beforeDispatch(action);
15+
dispatch<T>(action: AnyAction): T | Promise<T> {
16+
if (this.hook) {
17+
action = this.hook.before(action);
2118
}
2219

23-
let result = this.hook.dispatch(action) as T | Promise<T> | false;
24-
if (result !== false) {
25-
return result;
26-
}
20+
let result!: T | Promise<T>;
2721

2822
switch (action.type) {
2923
case "fetch": {
@@ -48,12 +42,15 @@ export class HttpActionDispatcher implements ActionDispatcher<HttpAction> {
4842
}
4943
}
5044

51-
/* eslint-disable unicorn/prefer-ternary, prettier/prettier */
52-
if (result instanceof Promise) {
53-
return result.then((result) => this.hook?.afterDispatch(action, result)) as Promise<T>;
54-
} else {
55-
return this.hook.afterDispatch(action, result) as T;
45+
if (this.hook) {
46+
/* eslint-disable unicorn/prefer-ternary, prettier/prettier */
47+
if (result instanceof Promise) {
48+
return result.then((result) => this.hook?.after(result, action)) as Promise<T>;
49+
} else {
50+
return this.hook?.after(result, action) as T;
51+
}
5652
}
57-
/* eslint-enable unicorn/prefer-ternary, prettier/prettier */
53+
54+
return result;
5855
}
5956
}

src/adapters/clients.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
import { type LogType } from "../interfaces";
1+
import {
2+
type ActionDispatcherHook,
3+
combine,
4+
type Hook,
5+
type HttpHook,
6+
type LogType,
7+
} from "../interfaces";
28
import { type mastodon } from "../mastodon";
39
import {
410
createActionProxy,
511
HttpActionDispatcher,
612
WebSocketActionDispatcher,
713
} from "./action";
8-
import { HttpActionDispatcherHookMastodon } from "./action/dispatcher-http-hook-mastodon";
914
import {
1015
HttpConfigImpl,
1116
type MastoHttpConfigProps,
1217
WebSocketConfigImpl,
1318
type WebSocketConfigProps,
1419
} from "./config";
20+
import { ActionDispatcherHookMastodon, HttpHookMastodon } from "./hook";
1521
import { HttpNativeImpl } from "./http";
1622
import { createLogger } from "./logger";
1723
import { SerializerNativeImpl } from "./serializers";
@@ -31,15 +37,40 @@ interface LogConfigProps {
3137
readonly log?: LogType;
3238
}
3339

40+
interface HookProps {
41+
readonly use?: readonly Hook[];
42+
}
43+
3444
export const createRestAPIClient = (
35-
props: MastoHttpConfigProps & LogConfigProps,
45+
props: MastoHttpConfigProps & LogConfigProps & HookProps,
3646
): mastodon.rest.Client => {
47+
const use = props.use ?? [];
48+
3749
const serializer = new SerializerNativeImpl();
3850
const config = new HttpConfigImpl(props, serializer);
3951
const logger = createLogger(props.log);
40-
const http = new HttpNativeImpl(serializer, config, logger);
41-
const hook = new HttpActionDispatcherHookMastodon(http);
42-
const actionDispatcher = new HttpActionDispatcher(http, hook);
52+
53+
const http = new HttpNativeImpl(
54+
serializer,
55+
config,
56+
logger,
57+
combine([
58+
...use.filter((hook): hook is HttpHook => hook.type === "Http"),
59+
new HttpHookMastodon(),
60+
]),
61+
);
62+
63+
const actionDispatcher = new HttpActionDispatcher(
64+
http,
65+
combine([
66+
...use.filter(
67+
(hook): hook is ActionDispatcherHook =>
68+
hook.type === "ActionDispatcher",
69+
),
70+
new ActionDispatcherHookMastodon(http),
71+
]),
72+
);
73+
4374
const actionProxy = createActionProxy(actionDispatcher, {
4475
context: ["api"],
4576
}) as mastodon.rest.Client;
@@ -53,8 +84,8 @@ export const createOAuthAPIClient = (
5384
const config = new HttpConfigImpl(props, serializer);
5485
const logger = createLogger(props.log);
5586
const http = new HttpNativeImpl(serializer, config, logger);
56-
const hook = new HttpActionDispatcherHookMastodon(http);
57-
const actionDispatcher = new HttpActionDispatcher(http, hook);
87+
const actionDispatcherHook = new ActionDispatcherHookMastodon(http);
88+
const actionDispatcher = new HttpActionDispatcher(http, actionDispatcherHook);
5889
const actionProxy = createActionProxy(actionDispatcher, {
5990
context: ["oauth"],
6091
}) as mastodon.oauth.Client;

src/adapters/action/dispatcher-http-hook-mastodon.ts renamed to src/adapters/hook/hook-action-dispatcher-mastodon.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
import { type mastodon } from "../../mastodon";
1111
import { isRecord, sleep } from "../../utils";
1212
import { MastoHttpError, MastoTimeoutError } from "../errors";
13-
import { type HttpAction, type HttpActionType } from "./dispatcher-http";
13+
14+
type HttpActionType = "fetch" | "create" | "update" | "remove" | "list";
1415

1516
function isHttpActionType(actionType: string): actionType is HttpActionType {
1617
return ["fetch", "create", "update", "remove", "list"].includes(actionType);
@@ -84,15 +85,15 @@ async function waitForMediaAttachment(
8485
return media;
8586
}
8687

87-
export class HttpActionDispatcherHookMastodon
88-
implements ActionDispatcherHook<AnyAction>
89-
{
88+
export class ActionDispatcherHookMastodon implements ActionDispatcherHook {
89+
readonly type = "ActionDispatcher";
90+
9091
constructor(
9192
private readonly http: Http,
9293
private readonly mediaTimeout = 1000 * 60,
9394
) {}
9495

95-
beforeDispatch(action: AnyAction): HttpAction {
96+
before(action: AnyAction): AnyAction {
9697
const type = toHttpActionType(action.type);
9798
const path = isHttpActionType(action.type)
9899
? action.path
@@ -103,18 +104,7 @@ export class HttpActionDispatcherHookMastodon
103104
return { type, path, data: action.data, meta };
104105
}
105106

106-
dispatch(action: AnyAction): false | Promise<unknown> {
107-
if (
108-
action.type === "update" &&
109-
action.path === "/api/v1/accounts/update_credentials"
110-
) {
111-
return this.http.patch(action.path, action.data, action.meta);
112-
}
113-
114-
return false;
115-
}
116-
117-
afterDispatch(action: AnyAction, result: unknown): unknown {
107+
after(result: unknown, action: AnyAction): unknown {
118108
if (action.type === "create" && action.path === "/api/v2/media") {
119109
const media = result as mastodon.v1.MediaAttachment;
120110
if (isRecord(action.data) && action.data?.skipPolling === true) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import assert from "node:assert";
2+
3+
import { HttpHookMastodon } from "./hook-http-mastodon";
4+
5+
describe("hookHttpMastodon", () => {
6+
it("returns a new Request with method PATCH if the request is PUT and the URL ends with /api/v1/accounts/update_credentials", async () => {
7+
const request = new Request(
8+
"https://example.com/api/v1/accounts/update_credentials",
9+
{ method: "PUT" },
10+
);
11+
const hook = new HttpHookMastodon();
12+
const result = await hook.before(request);
13+
assert(result instanceof Request);
14+
expect(result).toBeInstanceOf(Request);
15+
expect(result.method).toBe("PATCH");
16+
});
17+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { type HttpHook } from "../../interfaces/hook";
2+
3+
export class HttpHookMastodon implements HttpHook {
4+
readonly type = "Http";
5+
6+
async before(request: Request): Promise<Request> {
7+
if (
8+
request.method === "PUT" &&
9+
request.url.endsWith("/api/v1/accounts/update_credentials")
10+
) {
11+
return new Request(request, { method: "PATCH" });
12+
}
13+
return request;
14+
}
15+
16+
async after(response: Response): Promise<Response> {
17+
return response;
18+
}
19+
}

src/adapters/hook/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./hook-action-dispatcher-mastodon";
2+
export * from "./hook-http-mastodon";

src/adapters/http/http-native-impl.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
type Http,
33
type HttpConfig,
4+
type HttpHook,
45
type HttpRequestParams,
56
type HttpRequestResult,
67
type Logger,
@@ -20,20 +21,31 @@ export class HttpNativeImpl extends BaseHttp implements Http {
2021
private readonly serializer: Serializer,
2122
private readonly config: HttpConfig,
2223
private readonly logger?: Logger,
24+
private readonly hook?: HttpHook,
2325
) {
2426
super();
2527
}
2628

2729
async request(params: HttpRequestParams): Promise<HttpRequestResult> {
28-
const request = this.createRequest(params);
30+
let request = this.createRequest(params);
31+
32+
if (this.hook) {
33+
request = await this.hook.before(request);
34+
}
35+
36+
this.logger?.log("info", `↑ ${request.method} ${request.url}`);
37+
this.logger?.log("debug", "\tbody", {
38+
encoding: params.encoding,
39+
body: params.body,
40+
});
2941

3042
try {
31-
this.logger?.log("info", `↑ ${request.method} ${request.url}`);
32-
this.logger?.log("debug", "\tbody", {
33-
encoding: params.encoding,
34-
body: params.body,
35-
});
36-
const response = await fetch(request);
43+
let response = await fetch(request);
44+
45+
if (this.hook) {
46+
response = await this.hook.after(response);
47+
}
48+
3749
if (!response.ok) {
3850
throw response;
3951
}

src/interfaces/action.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export interface ActionDispatcher<T extends AnyAction> {
1515
[Symbol.dispose]?(): void;
1616
}
1717

18-
export interface ActionDispatcherHook<T extends AnyAction, U = unknown> {
19-
beforeDispatch(action: T): T;
20-
dispatch(action: T): U | Promise<U> | false;
21-
afterDispatch(action: T, result: U | Promise<U>): U;
22-
}
18+
// export interface ActionDispatcherHook<T extends AnyAction, U = unknown> {
19+
// beforeDispatch(action: T): T;
20+
// dispatch(action: T): U | Promise<U> | false;
21+
// afterDispatch(action: T, result: U | Promise<U>): U;
22+
// }

0 commit comments

Comments
 (0)