@shivaedev/github 0.8.6 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +47 -3
- package/dist/GitHub.d.ts +100 -17
- package/dist/GitHub.d.ts.map +1 -1
- package/dist/GitHub.js +4 -1
- package/dist/GitHub.js.map +1 -1
- package/dist/GitHubToken.d.ts +2 -2
- package/dist/GitHubToken.d.ts.map +1 -1
- package/dist/GitHubToken.js +1 -1
- package/dist/GitHubToken.js.map +1 -1
- package/dist/appTokens.d.ts +6 -0
- package/dist/appTokens.d.ts.map +1 -0
- package/dist/appTokens.js +64 -0
- package/dist/appTokens.js.map +1 -0
- package/dist/artifacts.d.ts +1 -1
- package/dist/authenticatedRequest.d.ts +1 -1
- package/dist/authenticatedRequest.d.ts.map +1 -1
- package/dist/authenticatedRequest.js +3 -2
- package/dist/authenticatedRequest.js.map +1 -1
- package/dist/cache.d.ts +2 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +3 -0
- package/dist/cache.js.map +1 -1
- package/dist/checkRuns.d.ts +116 -11
- package/dist/checkRuns.d.ts.map +1 -1
- package/dist/checkRuns.js +39 -18
- package/dist/checkRuns.js.map +1 -1
- package/dist/githubApp.d.ts +2 -4
- package/dist/githubApp.d.ts.map +1 -1
- package/dist/githubApp.js +5 -55
- package/dist/githubApp.js.map +1 -1
- package/dist/installationClients.d.ts +185 -0
- package/dist/installationClients.d.ts.map +1 -0
- package/dist/installationClients.js +28 -0
- package/dist/installationClients.js.map +1 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +4 -0
- package/dist/json.js.map +1 -1
- package/dist/representations.d.ts +4 -4
- package/dist/representations.d.ts.map +1 -1
- package/dist/representations.js +1 -1
- package/dist/representations.js.map +1 -1
- package/dist/requests.d.ts +2 -2
- package/dist/requests.d.ts.map +1 -1
- package/dist/requests.js +11 -9
- package/dist/requests.js.map +1 -1
- package/dist/status.d.ts +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/stickyComments.d.ts +1 -1
- package/package.json +7 -7
- package/src/GitHub.ts +15 -1
- package/src/GitHubToken.ts +4 -2
- package/src/appTokens.ts +92 -0
- package/src/authenticatedRequest.ts +3 -1
- package/src/cache.ts +5 -0
- package/src/checkRuns.ts +58 -18
- package/src/githubApp.ts +6 -80
- package/src/installationClients.ts +44 -0
- package/src/json.ts +4 -0
- package/src/representations.ts +2 -2
- package/src/requests.ts +11 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DateTime, Effect, Option, Redacted } from "effect";
|
|
1
|
+
import { DateTime, Effect, Option, Redacted, Stream } from "effect";
|
|
2
2
|
import { FetchHttpClient, type HttpClient, type HttpClientError, HttpClientRequest } from "effect/http";
|
|
3
3
|
import * as HttpStatus from "effect/http/HttpStatus";
|
|
4
4
|
import { GitHubTransportError } from "./errors.ts";
|
|
@@ -30,12 +30,14 @@ export function authenticatedRequest<R>(
|
|
|
30
30
|
const credential = yield* accessToken;
|
|
31
31
|
let response = yield* send(credential);
|
|
32
32
|
if (isUnauthorized(response) && refresh !== undefined) {
|
|
33
|
+
yield* Effect.scoped(Stream.toPull(response.stream));
|
|
33
34
|
yield* refresh(credential);
|
|
34
35
|
response = yield* send(yield* accessToken);
|
|
35
36
|
}
|
|
36
37
|
const observedResource = rates.observe(response);
|
|
37
38
|
if (!((mode === "revalidate" && isUnchanged(response)) || (mode === "manual" && response.status === HttpStatus.fromLiteral("Found")))) {
|
|
38
39
|
yield* checkStatus(response, observedResource, rates.failures.get(observedResource) ?? 0).pipe(
|
|
40
|
+
Effect.onError(() => Effect.scoped(Stream.toPull(response.stream))),
|
|
39
41
|
Effect.catchTag("HttpClientError", (cause) => Effect.fail(new GitHubTransportError({ path }, { cause }))),
|
|
40
42
|
Effect.catchTag("GitHubRateLimited", (error) => {
|
|
41
43
|
rates.blocked.set(error.resource, error);
|
package/src/cache.ts
CHANGED
|
@@ -4,9 +4,14 @@ import type { HttpClientResponse } from "effect/http";
|
|
|
4
4
|
export interface CachedResponse {
|
|
5
5
|
readonly bytes: Uint8Array;
|
|
6
6
|
readonly decoded: Map<Schema.Constraint, unknown>;
|
|
7
|
+
readonly etag?: string | undefined;
|
|
7
8
|
readonly response: HttpClientResponse.HttpClientResponse;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
export function revalidatedResponse(cached: CachedResponse, response: HttpClientResponse.HttpClientResponse): CachedResponse {
|
|
12
|
+
return { ...cached, etag: response.headers.etag ?? cached.etag, response };
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export function responseCache() {
|
|
11
16
|
const entries = new Map<string, CachedResponse>();
|
|
12
17
|
return {
|
package/src/checkRuns.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
1
|
+
import { Effect, Option, Schema, Stream } from "effect";
|
|
2
2
|
import type { Representations } from "#representations.ts";
|
|
3
3
|
|
|
4
4
|
export const CheckRunOutput = Schema.Struct({
|
|
@@ -8,16 +8,22 @@ export const CheckRunOutput = Schema.Struct({
|
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
export const CheckRunConclusion = Schema.Literals(["action_required", "cancelled", "failure", "neutral", "success", "skipped", "stale", "timed_out"]);
|
|
11
|
-
export const CheckRunStatus = Schema.Literals(["queued", "in_progress", "completed"]);
|
|
11
|
+
export const CheckRunStatus = Schema.Literals(["queued", "in_progress", "completed", "waiting", "requested", "pending"]);
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
conclusion: Schema.optional(CheckRunConclusion),
|
|
13
|
+
const createFields = {
|
|
15
14
|
detailsUrl: Schema.optional(Schema.String),
|
|
16
15
|
headSha: Schema.String,
|
|
17
16
|
name: Schema.String,
|
|
18
17
|
output: Schema.optional(CheckRunOutput),
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
};
|
|
19
|
+
export const CreateCheckRun = Schema.Union([
|
|
20
|
+
Schema.Struct({ ...createFields, conclusion: Schema.optional(CheckRunConclusion), status: Schema.Literals(["queued", "in_progress"]) }).pipe(
|
|
21
|
+
Schema.encodeKeys({ detailsUrl: "details_url", headSha: "head_sha" }),
|
|
22
|
+
),
|
|
23
|
+
Schema.Struct({ ...createFields, conclusion: CheckRunConclusion, status: Schema.Literal("completed") }).pipe(
|
|
24
|
+
Schema.encodeKeys({ detailsUrl: "details_url", headSha: "head_sha" }),
|
|
25
|
+
),
|
|
26
|
+
]);
|
|
21
27
|
|
|
22
28
|
export const UpdateCheckRun = Schema.Union([
|
|
23
29
|
Schema.Struct({
|
|
@@ -34,6 +40,7 @@ export const UpdateCheckRun = Schema.Union([
|
|
|
34
40
|
]);
|
|
35
41
|
|
|
36
42
|
export const CheckRun = Schema.Struct({
|
|
43
|
+
app: Schema.optional(Schema.NullOr(Schema.Struct({ id: Schema.Number }))),
|
|
37
44
|
conclusion: Schema.NullOr(CheckRunConclusion),
|
|
38
45
|
detailsUrl: Schema.NullOr(Schema.String),
|
|
39
46
|
headSha: Schema.String,
|
|
@@ -43,17 +50,50 @@ export const CheckRun = Schema.Struct({
|
|
|
43
50
|
status: CheckRunStatus,
|
|
44
51
|
}).pipe(Schema.encodeKeys({ detailsUrl: "details_url", headSha: "head_sha" }));
|
|
45
52
|
|
|
53
|
+
const CheckRunPage = Schema.Struct({ checkRuns: Schema.Array(CheckRun) }).pipe(Schema.encodeKeys({ checkRuns: "check_runs" }));
|
|
54
|
+
|
|
55
|
+
export const FindCheckRun = Schema.Union([
|
|
56
|
+
Schema.Struct({ appId: Schema.optional(Schema.Number), headSha: Schema.String, name: Schema.String }),
|
|
57
|
+
Schema.Struct({ appId: Schema.Number, headSha: Schema.String, name: Schema.optional(Schema.String) }),
|
|
58
|
+
]);
|
|
59
|
+
|
|
46
60
|
export function checkRuns(client: Representations) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const createCheckRun = Effect.fn("GitHub.createCheckRun")(function* (owner: string, repo: string, input: typeof CreateCheckRun.Type) {
|
|
62
|
+
const body = yield* Schema.encodeEffect(CreateCheckRun)(input);
|
|
63
|
+
const response = yield* client.write("POST", `/repos/${owner}/${repo}/check-runs`, body);
|
|
64
|
+
return yield* response.decode(CheckRun);
|
|
65
|
+
});
|
|
66
|
+
const updateCheckRun = Effect.fn("GitHub.updateCheckRun")(function* (owner: string, repo: string, id: number, input: typeof UpdateCheckRun.Type) {
|
|
67
|
+
const body = yield* Schema.encodeEffect(UpdateCheckRun)(input);
|
|
68
|
+
const response = yield* client.write("PATCH", `/repos/${owner}/${repo}/check-runs/${id}`, body);
|
|
69
|
+
return yield* response.decode(CheckRun);
|
|
70
|
+
});
|
|
71
|
+
const findLatestCheckRun = Effect.fn("GitHub.findLatestCheckRun")(function* (owner: string, repo: string, input: typeof FindCheckRun.Type) {
|
|
72
|
+
const { headSha, name, appId } = yield* Schema.decodeUnknownEffect(FindCheckRun)(input);
|
|
73
|
+
const query = new URLSearchParams({ filter: "all", "per_page": "100" });
|
|
74
|
+
if (name !== undefined) {
|
|
75
|
+
query.set("check_name", name);
|
|
76
|
+
}
|
|
77
|
+
const pages = yield* client.list(`/repos/${owner}/${repo}/commits/${encodeURIComponent(headSha)}/check-runs?${query}`);
|
|
78
|
+
return yield* pages.pipe(
|
|
79
|
+
Stream.mapEffect((page) => page.decode(CheckRunPage)),
|
|
80
|
+
Stream.flatMap((page) => Stream.fromIterable(page.checkRuns)),
|
|
81
|
+
Stream.filter((run) => (name === undefined || run.name === name) && (appId === undefined || run.app?.id === appId)),
|
|
82
|
+
Stream.runFold(
|
|
83
|
+
() => Option.none<typeof CheckRun.Type>(),
|
|
84
|
+
(latest, run) => (Option.isNone(latest) || run.id > latest.value.id ? Option.some(run) : latest),
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
const upsertCheckRun = Effect.fn("GitHub.upsertCheckRun")(function* (
|
|
89
|
+
owner: string,
|
|
90
|
+
repo: string,
|
|
91
|
+
input: typeof CreateCheckRun.Type,
|
|
92
|
+
appId?: number,
|
|
93
|
+
) {
|
|
94
|
+
yield* Schema.encodeEffect(CreateCheckRun)(input);
|
|
95
|
+
const existing = yield* findLatestCheckRun(owner, repo, { appId, headSha: input.headSha, name: input.name });
|
|
96
|
+
return yield* Option.isSome(existing) ? updateCheckRun(owner, repo, existing.value.id, input) : createCheckRun(owner, repo, input);
|
|
97
|
+
});
|
|
98
|
+
return { createCheckRun, findLatestCheckRun, updateCheckRun, upsertCheckRun };
|
|
59
99
|
}
|
package/src/githubApp.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { type AppCredentials, appJwt } from "#appJwt.ts";
|
|
6
|
-
import { GitHubAppError } from "#errors.ts";
|
|
7
|
-
import { GitHubConfig } from "#GitHubConfig.ts";
|
|
8
|
-
import { GitHubToken } from "#GitHubToken.ts";
|
|
9
|
-
import { TokenRefresh } from "#TokenRefresh.ts";
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import type { AppCredentials } from "#appJwt.ts";
|
|
3
|
+
import { appTokens } from "#appTokens.ts";
|
|
4
|
+
import { GitHubApp, GitHubAppCredentials } from "#installationClients.ts";
|
|
10
5
|
|
|
11
6
|
export interface InstallationScope {
|
|
12
7
|
readonly permissions?: Readonly<Record<string, "read" | "write" | "admin">>;
|
|
@@ -21,76 +16,7 @@ export type GitHubAppOptions = AppCredentials &
|
|
|
21
16
|
| { readonly installationId?: never; readonly owner?: never; readonly repository?: never; readonly scope?: never }
|
|
22
17
|
);
|
|
23
18
|
|
|
24
|
-
const Installation = Schema.Struct({ id: Schema.Number });
|
|
25
|
-
const AccessToken = Schema.Struct({ expiresAt: Schema.DateFromString, token: Schema.RedactedFromValue(Schema.String) }).pipe(
|
|
26
|
-
Schema.encodeKeys({ expiresAt: "expires_at" }),
|
|
27
|
-
);
|
|
28
|
-
|
|
29
19
|
export function githubApp(options: GitHubAppOptions) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!("installationId" in options || "owner" in options)) {
|
|
33
|
-
return Context.make(GitHubToken, Effect.map(appJwt(options), Option.some)).pipe(Context.add(TokenRefresh, undefined));
|
|
34
|
-
}
|
|
35
|
-
const client = yield* HttpClient.HttpClient;
|
|
36
|
-
const { apiUrl } = yield* GitHubConfig;
|
|
37
|
-
const clock = yield* Clock.Clock;
|
|
38
|
-
let installationId = "installationId" in options ? options.installationId : undefined;
|
|
39
|
-
const request = appRequests(client, apiUrl);
|
|
40
|
-
const exchange = Effect.gen(function* () {
|
|
41
|
-
const jwt = yield* appJwt(options);
|
|
42
|
-
if (installationId === undefined && "owner" in options && options.owner !== undefined) {
|
|
43
|
-
const path = `/repos/${encodeURIComponent(options.owner)}/${encodeURIComponent(options.repository)}/installation`;
|
|
44
|
-
installationId = (yield* request("GET", path, jwt, Installation)).id;
|
|
45
|
-
}
|
|
46
|
-
return yield* request("POST", `/app/installations/${installationId}/access_tokens`, jwt, AccessToken, {
|
|
47
|
-
permissions: options.scope?.permissions,
|
|
48
|
-
repositories: options.scope?.repositories,
|
|
49
|
-
"repository_ids": options.scope?.repositoryIds,
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
const cache = yield* Cache.makeWith((_: undefined) => exchange, {
|
|
53
|
-
capacity: 1,
|
|
54
|
-
timeToLive: (exit) => (Exit.isSuccess(exit) ? Math.max(0, exit.value.expiresAt.getTime() - clock.currentTimeMillisUnsafe() - 300_000) : 0),
|
|
55
|
-
});
|
|
56
|
-
return Context.make(
|
|
57
|
-
GitHubToken,
|
|
58
|
-
Effect.map(Cache.get(cache, undefined), ({ token }) => Option.some(token)),
|
|
59
|
-
).pipe(
|
|
60
|
-
Context.add(TokenRefresh, (rejected) =>
|
|
61
|
-
Cache.invalidateWhen(cache, undefined, ({ token }) => Option.isSome(rejected) && token === rejected.value),
|
|
62
|
-
),
|
|
63
|
-
);
|
|
64
|
-
}),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function appRequests(client: HttpClient.HttpClient, apiUrl: string) {
|
|
69
|
-
const checked = filterStatus(client);
|
|
70
|
-
return Effect.fnUntraced(function* <TSchema extends Schema.Constraint>(
|
|
71
|
-
method: "GET" | "POST",
|
|
72
|
-
path: string,
|
|
73
|
-
jwt: Redacted.Redacted<string>,
|
|
74
|
-
schema: TSchema,
|
|
75
|
-
body?: unknown,
|
|
76
|
-
) {
|
|
77
|
-
let input = HttpClientRequest.make(method)(new URL(path, apiUrl).href, {
|
|
78
|
-
headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${Redacted.value(jwt)}`, "X-GitHub-Api-Version": "2022-11-28" },
|
|
79
|
-
});
|
|
80
|
-
if (body !== undefined) {
|
|
81
|
-
input = yield* HttpClientRequest.bodyJson(input, body).pipe(Effect.mapError((cause) => new GitHubAppError({ reason: "encode" }, { cause })));
|
|
82
|
-
}
|
|
83
|
-
const response = yield* checked
|
|
84
|
-
.execute(input)
|
|
85
|
-
.pipe(
|
|
86
|
-
Effect.mapError((cause) =>
|
|
87
|
-
cause._tag === "HttpClientError"
|
|
88
|
-
? new GitHubAppError({ reason: "transport" }, { cause })
|
|
89
|
-
: new GitHubAppError({ reason: "status", status: cause.status }),
|
|
90
|
-
),
|
|
91
|
-
);
|
|
92
|
-
return yield* readBoundedJson(response, 64 * 1024, schema).pipe(
|
|
93
|
-
Effect.mapError((cause) => new GitHubAppError({ reason: cause._tag === "HttpClientError" ? "transport" : "response" }, { cause })),
|
|
94
|
-
);
|
|
95
|
-
});
|
|
20
|
+
const authentication = appTokens(options);
|
|
21
|
+
return Layer.merge(authentication, GitHubApp.layer.pipe(Layer.provide(Layer.succeed(GitHubAppCredentials)({ credentials: options }))));
|
|
96
22
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Cache, Context, Effect, Layer } from "effect";
|
|
2
|
+
import { HttpClient } from "effect/http";
|
|
3
|
+
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
4
|
+
import type { AppCredentials } from "#appJwt.ts";
|
|
5
|
+
import { appTokens } from "#appTokens.ts";
|
|
6
|
+
import { GitHub } from "#GitHub.ts";
|
|
7
|
+
import { GitHubConfig } from "#GitHubConfig.ts";
|
|
8
|
+
|
|
9
|
+
export const GitHubAppCredentials = Context.Service<{ readonly credentials: AppCredentials }>("@shivaedev/github/AppCredentials");
|
|
10
|
+
|
|
11
|
+
export const GitHubApp = defineService({
|
|
12
|
+
id: "@shivaedev/github/GitHubApp",
|
|
13
|
+
initialize: Effect.gen(function* () {
|
|
14
|
+
const { credentials } = yield* GitHubAppCredentials;
|
|
15
|
+
const http = yield* HttpClient.HttpClient;
|
|
16
|
+
const config = yield* GitHubConfig;
|
|
17
|
+
return yield* Cache.make({
|
|
18
|
+
capacity: 100,
|
|
19
|
+
lookup: (installationId: number) =>
|
|
20
|
+
Layer.build(
|
|
21
|
+
GitHub.layer.pipe(
|
|
22
|
+
Layer.provide(appTokens({ ...credentials, installationId })),
|
|
23
|
+
Layer.provide(Layer.succeed(HttpClient.HttpClient)(http)),
|
|
24
|
+
Layer.provide(Layer.succeed(GitHubConfig)(config)),
|
|
25
|
+
),
|
|
26
|
+
).pipe(
|
|
27
|
+
Effect.scoped,
|
|
28
|
+
Effect.map((context) => Context.get(context, GitHub)),
|
|
29
|
+
),
|
|
30
|
+
timeToLive: "30 minutes",
|
|
31
|
+
});
|
|
32
|
+
}),
|
|
33
|
+
methods: (clients) => ({
|
|
34
|
+
installation: (installationId: number) =>
|
|
35
|
+
Cache.get(clients, installationId).pipe(Effect.tap((client) => Cache.set(clients, installationId, client))),
|
|
36
|
+
}),
|
|
37
|
+
requires: [GitHubAppCredentials, HttpClient.HttpClient, GitHubConfig],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export function withInstallation<A, E, R>(installationId: number, effect: Effect.Effect<A, E, R>) {
|
|
41
|
+
return Effect.flatMap(GitHubApp, (app) =>
|
|
42
|
+
Effect.flatMap(app.installation(installationId), (client) => Effect.provideService(effect, GitHub, client)),
|
|
43
|
+
);
|
|
44
|
+
}
|
package/src/json.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
+
import * as HttpStatus from "effect/http/HttpStatus";
|
|
2
3
|
import type { CachedResponse } from "./cache.ts";
|
|
3
4
|
|
|
4
5
|
export const decodeJson = Effect.fnUntraced(function* <TSchema extends Schema.Constraint>(entry: CachedResponse, schema: TSchema) {
|
|
6
|
+
if (entry.response.status === HttpStatus.fromLiteral("NoContent")) {
|
|
7
|
+
return yield* Schema.decodeUnknownEffect(schema)(undefined);
|
|
8
|
+
}
|
|
5
9
|
const cached = entry.decoded.get(schema);
|
|
6
10
|
if (entry.decoded.has(schema) && Schema.is(schema)(cached)) {
|
|
7
11
|
return cached;
|
package/src/representations.ts
CHANGED
|
@@ -45,9 +45,9 @@ export function representations(
|
|
|
45
45
|
),
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
|
-
function writeRepresentation(method: "POST" | "PATCH", path: string, body
|
|
48
|
+
function writeRepresentation(method: "POST" | "PATCH" | "PUT" | "DELETE", path: string, body?: unknown, byteLimit = responseByteLimit) {
|
|
49
49
|
return HttpClientRequest.make(method)(new URL(path, apiUrl).href, { headers: { Accept: "application/vnd.github+json" } }).pipe(
|
|
50
|
-
HttpClientRequest.bodyJson(body),
|
|
50
|
+
(input) => (body === undefined ? Effect.succeed(input) : HttpClientRequest.bodyJson(input, body)),
|
|
51
51
|
Effect.flatMap((input) => request(input, byteLimit)),
|
|
52
52
|
Effect.map(jsonResponse),
|
|
53
53
|
);
|
package/src/requests.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
3
3
|
import { readBoundedBody } from "@shivaedev/http/body.ts";
|
|
4
4
|
import { ResponseBodyTooLarge } from "@shivaedev/http/errors.ts";
|
|
5
5
|
import { authenticatedRequest } from "./authenticatedRequest.ts";
|
|
6
|
-
import { type CachedResponse, responseCache } from "./cache.ts";
|
|
6
|
+
import { type CachedResponse, responseCache, revalidatedResponse } from "./cache.ts";
|
|
7
7
|
import { GitHubTransportError } from "./errors.ts";
|
|
8
8
|
import type { GitHubToken } from "./GitHubToken.ts";
|
|
9
9
|
import type { RateLimits } from "./rateLimits.ts";
|
|
@@ -17,28 +17,30 @@ export function requests(
|
|
|
17
17
|
refresh: typeof TokenRefresh.Service = undefined,
|
|
18
18
|
) {
|
|
19
19
|
const cache = responseCache();
|
|
20
|
-
const send = authenticatedRequest(client, token, rates, refresh);
|
|
20
|
+
const send = authenticatedRequest(HttpClient.withScope(client), token, rates, refresh);
|
|
21
21
|
return Effect.fn("GitHub.request")(function* (input: HttpClientRequest.HttpClientRequest, limit: number) {
|
|
22
|
-
const key =
|
|
22
|
+
const key = JSON.stringify([input.url, input.urlParams, input.headers.accept]);
|
|
23
23
|
const cached = input.method === "GET" ? cache.get(key) : undefined;
|
|
24
24
|
let request = HttpClientRequest.removeHeader(input, "if-none-match");
|
|
25
|
-
if (cached?.
|
|
26
|
-
request = HttpClientRequest.setHeader(request, "If-None-Match", cached.
|
|
25
|
+
if (cached?.etag !== undefined) {
|
|
26
|
+
request = HttpClientRequest.setHeader(request, "If-None-Match", cached.etag);
|
|
27
27
|
}
|
|
28
28
|
const response = yield* send(request, cached === undefined ? undefined : "revalidate");
|
|
29
29
|
if (isUnchanged(response) && cached !== undefined) {
|
|
30
30
|
if (cached.bytes.byteLength > limit) {
|
|
31
31
|
return yield* new ResponseBodyTooLarge({ limit });
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
const entry = revalidatedResponse(cached, response);
|
|
34
|
+
cache.set(key, entry);
|
|
35
|
+
return entry;
|
|
34
36
|
}
|
|
35
37
|
const bytes = yield* readBoundedBody(response, limit).pipe(
|
|
36
38
|
Effect.catchTag("HttpClientError", (cause) => Effect.fail(new GitHubTransportError({ path: new URL(input.url).pathname }, { cause }))),
|
|
37
39
|
);
|
|
38
|
-
const entry: CachedResponse = { bytes, decoded: new Map(), response };
|
|
40
|
+
const entry: CachedResponse = { bytes, decoded: new Map(), etag: response.headers.etag, response };
|
|
39
41
|
if (input.method === "GET" && response.headers.etag !== undefined) {
|
|
40
42
|
cache.set(key, entry);
|
|
41
43
|
}
|
|
42
44
|
return entry;
|
|
43
|
-
});
|
|
45
|
+
}, Effect.scoped);
|
|
44
46
|
}
|