Skip to content

Commit ea74b3c

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

File tree

11 files changed

+136
-62
lines changed

11 files changed

+136
-62
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: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import {
55
HttpActionDispatcher,
66
WebSocketActionDispatcher,
77
} from "./action";
8-
import { HttpActionDispatcherHookMastodon } from "./action/dispatcher-http-hook-mastodon";
98
import {
109
HttpConfigImpl,
1110
type MastoHttpConfigProps,
1211
WebSocketConfigImpl,
1312
type WebSocketConfigProps,
1413
} from "./config";
14+
import { ActionDispatcherHookMastodon, HttpHookMastodon } from "./hook";
1515
import { HttpNativeImpl } from "./http";
1616
import { createLogger } from "./logger";
1717
import { SerializerNativeImpl } from "./serializers";
@@ -37,9 +37,10 @@ export const createRestAPIClient = (
3737
const serializer = new SerializerNativeImpl();
3838
const config = new HttpConfigImpl(props, serializer);
3939
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);
40+
const httpHook = new HttpHookMastodon();
41+
const http = new HttpNativeImpl(serializer, config, logger, httpHook);
42+
const actionDispatcherHook = new ActionDispatcherHookMastodon(http);
43+
const actionDispatcher = new HttpActionDispatcher(http, actionDispatcherHook);
4344
const actionProxy = createActionProxy(actionDispatcher, {
4445
context: ["api"],
4546
}) as mastodon.rest.Client;
@@ -53,8 +54,8 @@ export const createOAuthAPIClient = (
5354
const config = new HttpConfigImpl(props, serializer);
5455
const logger = createLogger(props.log);
5556
const http = new HttpNativeImpl(serializer, config, logger);
56-
const hook = new HttpActionDispatcherHookMastodon(http);
57-
const actionDispatcher = new HttpActionDispatcher(http, hook);
57+
const actionDispatcherHook = new ActionDispatcherHookMastodon(http);
58+
const actionDispatcher = new HttpActionDispatcher(http, actionDispatcherHook);
5859
const actionProxy = createActionProxy(actionDispatcher, {
5960
context: ["oauth"],
6061
}) 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.toString().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+
// }

src/interfaces/hook.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { type AnyAction } from "./action";
2+
3+
export type BeforeFn<Ctx extends unknown[], Result> = (...args: Ctx) => Result;
4+
5+
export type AfterFn<Ctx extends unknown[], Result> = (...args: Ctx) => Result;
6+
7+
export interface Hook<
8+
T,
9+
BCtx extends unknown[],
10+
BRes,
11+
ACtx extends unknown[],
12+
ARes,
13+
> {
14+
type: T;
15+
before: BeforeFn<BCtx, BRes>;
16+
after: AfterFn<ACtx, ARes>;
17+
}
18+
19+
export type AnyHook = Hook<string, [unknown], unknown, [unknown], unknown>;
20+
21+
export type HttpHook = Hook<
22+
"Http",
23+
[Request],
24+
Promise<Request>,
25+
[Response],
26+
Promise<Response>
27+
>;
28+
29+
export type ActionDispatcherHook = Hook<
30+
"ActionDispatcher",
31+
[AnyAction],
32+
AnyAction,
33+
[unknown, AnyAction],
34+
unknown
35+
>;

src/interfaces/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
export * from "./action";
2+
export * from "./config";
3+
export * from "./hook";
14
export * from "./http";
25
export * from "./logger";
36
export * from "./serializer";
4-
export * from "./config";
5-
export * from "./action";
67
export * from "./ws";

0 commit comments

Comments
 (0)