@shivaedev/pr-watch 0.1.2 → 0.3.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 +23 -0
- package/README.md +20 -5
- package/dist/GitHub.d.ts.map +1 -1
- package/dist/GitHub.js +5 -19
- package/dist/GitHub.js.map +1 -1
- package/dist/GitHubHttp.d.ts +5 -6
- package/dist/GitHubHttp.d.ts.map +1 -1
- package/dist/GitHubHttp.js +34 -29
- package/dist/GitHubHttp.js.map +1 -1
- package/dist/Watcher.d.ts +1 -1
- package/dist/Watcher.d.ts.map +1 -1
- package/dist/Watcher.js +4 -4
- package/dist/Watcher.js.map +1 -1
- package/dist/arguments.d.ts +4 -4
- package/dist/arguments.d.ts.map +1 -1
- package/dist/arguments.js +19 -3
- package/dist/arguments.js.map +1 -1
- package/dist/cli.js +13 -12
- package/dist/cli.js.map +1 -1
- package/dist/confirmQueue.d.ts +3 -2
- package/dist/confirmQueue.d.ts.map +1 -1
- package/dist/confirmQueue.js +2 -2
- package/dist/confirmQueue.js.map +1 -1
- package/dist/events.d.ts +4 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -2
- package/dist/events.js.map +1 -1
- package/dist/health.d.ts +6 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +19 -0
- package/dist/health.js.map +1 -0
- package/dist/model.d.ts +3 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +1 -0
- package/dist/model.js.map +1 -1
- package/dist/program.d.ts +3 -2
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +42 -23
- package/dist/program.js.map +1 -1
- package/dist/quota.d.ts +3 -9
- package/dist/quota.d.ts.map +1 -1
- package/dist/quota.js +10 -30
- package/dist/quota.js.map +1 -1
- package/package.json +5 -4
- package/src/GitHub.ts +5 -19
- package/src/GitHubHttp.ts +47 -31
- package/src/Watcher.ts +5 -5
- package/src/arguments.ts +20 -3
- package/src/cli.ts +19 -16
- package/src/confirmQueue.ts +2 -2
- package/src/events.ts +11 -6
- package/src/health.ts +26 -0
- package/src/model.ts +3 -1
- package/src/program.ts +56 -26
- package/src/quota.ts +16 -37
- package/dist/httpResponse.d.ts +0 -15
- package/dist/httpResponse.d.ts.map +0 -1
- package/dist/httpResponse.js +0 -31
- package/dist/httpResponse.js.map +0 -1
- package/src/httpResponse.ts +0 -37
package/src/cli.ts
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { NodeFileSystem } from "@effect/platform-node";
|
|
4
|
-
import {
|
|
4
|
+
import { Effect, Layer, Logger } from "effect";
|
|
5
5
|
import { FetchHttpClient } from "effect/http";
|
|
6
|
-
import { GitHub } from "
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
6
|
+
import { GitHub as GitHubClient } from "@shivaedev/effect-github/GitHub.ts";
|
|
7
|
+
import { GitHubToken } from "@shivaedev/effect-github/GitHubToken.ts";
|
|
8
|
+
import { GitHub } from "#GitHub.ts";
|
|
9
|
+
import { GitHubHttp } from "#GitHubHttp.ts";
|
|
10
|
+
import { WatchError } from "#model.ts";
|
|
11
|
+
import { program } from "#program.ts";
|
|
12
|
+
import { StateFile } from "#StateFile.ts";
|
|
13
|
+
import { Watcher } from "#Watcher.ts";
|
|
12
14
|
|
|
13
15
|
const services = Watcher.layer.pipe(
|
|
14
|
-
Layer.provide(
|
|
16
|
+
Layer.provide(
|
|
17
|
+
Layer.mergeAll(
|
|
18
|
+
StateFile.layer,
|
|
19
|
+
GitHub.layer.pipe(Layer.provide(GitHubHttp.layer.pipe(Layer.provide(GitHubClient.layer), Layer.provide(GitHubToken.fromEnv)))),
|
|
20
|
+
),
|
|
21
|
+
),
|
|
15
22
|
Layer.provide(
|
|
16
23
|
Layer.mergeAll(
|
|
17
24
|
NodeFileSystem.layer,
|
|
@@ -31,14 +38,10 @@ process.once("SIGINT", () => abort.abort());
|
|
|
31
38
|
process.once("SIGTERM", () => abort.abort());
|
|
32
39
|
try {
|
|
33
40
|
await Effect.runPromise(
|
|
34
|
-
Effect.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
);
|
|
39
|
-
yield* program(process.argv.slice(2), token, write(process.stdout), write(process.stderr));
|
|
40
|
-
}).pipe(Effect.provide(services)),
|
|
41
|
-
{ signal: abort.signal },
|
|
41
|
+
program(process.argv.slice(2), write(process.stdout), write(process.stderr)).pipe(Effect.provide(services), Effect.provide(Logger.layer([]))),
|
|
42
|
+
{
|
|
43
|
+
signal: abort.signal,
|
|
44
|
+
},
|
|
42
45
|
);
|
|
43
46
|
} catch (cause) {
|
|
44
47
|
if (!abort.signal.aborted) {
|
package/src/confirmQueue.ts
CHANGED
|
@@ -40,8 +40,8 @@ export const confirmQueue = Effect.fn("GitHub.confirmQueue")(function* (
|
|
|
40
40
|
url.pathname = `${url.pathname.replace(/\/$/u, "")}/graphql`;
|
|
41
41
|
}
|
|
42
42
|
const [owner, name] = options.repository.split("/");
|
|
43
|
-
const response = yield* http.post(url.href,
|
|
44
|
-
const body = yield* Schema.decodeUnknownEffect(QueueResponse)(response
|
|
43
|
+
const response = yield* http.post(url.href, { query, variables: { name, number: snapshot.number, owner } });
|
|
44
|
+
const body = yield* Schema.decodeUnknownEffect(QueueResponse)(response).pipe(
|
|
45
45
|
Effect.mapError(() => new WatchError({ message: "Invalid GitHub queue confirmation" })),
|
|
46
46
|
);
|
|
47
47
|
const pr = body.data?.repository?.pullRequest;
|
package/src/events.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { Snapshot } from "
|
|
1
|
+
import type { Snapshot } from "#model.ts";
|
|
2
|
+
|
|
3
|
+
export const eventKinds = ["merged", "closed", "conflict", "queued", "dequeued", "auto-merge-off", "checks-failed", "stuck"] as const;
|
|
4
|
+
export type EventKind = (typeof eventKinds)[number];
|
|
2
5
|
|
|
3
6
|
function stuck(pr: Snapshot): boolean {
|
|
4
7
|
return pr.state === "open" && pr.autoMerge && (pr.dirty || pr.failed);
|
|
5
8
|
}
|
|
6
|
-
function openEvents(before: Snapshot, after: Snapshot): readonly
|
|
7
|
-
const transitions: readonly (readonly [boolean,
|
|
9
|
+
function openEvents(before: Snapshot, after: Snapshot): readonly EventKind[] {
|
|
10
|
+
const transitions: readonly (readonly [boolean, EventKind])[] = [
|
|
8
11
|
[!before.dirty && after.dirty, "conflict"],
|
|
9
12
|
[!before.queued && after.queued, "queued"],
|
|
10
13
|
[before.queued && !after.queued, "dequeued"],
|
|
@@ -14,12 +17,12 @@ function openEvents(before: Snapshot, after: Snapshot): readonly string[] {
|
|
|
14
17
|
];
|
|
15
18
|
return transitions.flatMap(([changed, event]) => (changed ? [event] : []));
|
|
16
19
|
}
|
|
17
|
-
export function events(before: Snapshot | undefined, after: Snapshot): readonly string[] {
|
|
20
|
+
export function events(before: Snapshot | undefined, after: Snapshot, selected?: readonly EventKind[]): readonly string[] {
|
|
18
21
|
if (before === undefined && !stuck(after)) {
|
|
19
22
|
return [];
|
|
20
23
|
}
|
|
21
24
|
const baseline = before ?? { ...after, autoMerge: false, dirty: false, failed: false };
|
|
22
|
-
const found:
|
|
25
|
+
const found: EventKind[] = [];
|
|
23
26
|
if (!baseline.merged && after.merged) {
|
|
24
27
|
found.push("merged");
|
|
25
28
|
}
|
|
@@ -29,5 +32,7 @@ export function events(before: Snapshot | undefined, after: Snapshot): readonly
|
|
|
29
32
|
if (after.state === "open") {
|
|
30
33
|
found.push(...openEvents(baseline, after));
|
|
31
34
|
}
|
|
32
|
-
return found
|
|
35
|
+
return found
|
|
36
|
+
.filter((event) => selected === undefined || selected.includes(event))
|
|
37
|
+
.map((event) => JSON.stringify({ branch: after.branch, event, number: after.number, title: after.title }));
|
|
33
38
|
}
|
package/src/health.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Clock, Effect } from "effect";
|
|
2
|
+
import type { WatchError } from "#model.ts";
|
|
3
|
+
|
|
4
|
+
export type Emit = (line: string) => Effect.Effect<void, WatchError>;
|
|
5
|
+
export const health = Effect.fnUntraced(function* (
|
|
6
|
+
emit: Emit,
|
|
7
|
+
event: "heartbeat" | "cooldown" | "error" | "stopped",
|
|
8
|
+
fields: Record<string, unknown> = {},
|
|
9
|
+
) {
|
|
10
|
+
const at = new Date(yield* Clock.currentTimeMillis).toISOString();
|
|
11
|
+
yield* emit(JSON.stringify({ at, event, ...fields }));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const waitForPoll = Effect.fnUntraced(function* (emit: Emit, interval: number, delay: number) {
|
|
15
|
+
const next = (yield* Clock.currentTimeMillis) + delay;
|
|
16
|
+
const nextPollAt = new Date(next).toISOString();
|
|
17
|
+
if (delay > interval) {
|
|
18
|
+
yield* health(emit, "cooldown", { delayMs: delay, message: "GitHub quota pacing", nextPollAt });
|
|
19
|
+
}
|
|
20
|
+
let remaining = delay;
|
|
21
|
+
while (remaining > 0) {
|
|
22
|
+
yield* health(emit, "heartbeat", { nextPollAt });
|
|
23
|
+
yield* Effect.sleep(Math.min(interval, remaining));
|
|
24
|
+
remaining = next - (yield* Clock.currentTimeMillis);
|
|
25
|
+
}
|
|
26
|
+
});
|
package/src/model.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import type { EventKind } from "#events.ts";
|
|
2
3
|
export class WatchError extends Schema.TaggedError<WatchError>()("WatchError", {
|
|
3
4
|
message: Schema.String,
|
|
5
|
+
retryAt: Schema.optionalKey(Schema.Number),
|
|
4
6
|
}) {}
|
|
5
7
|
export const Snapshot = Schema.Struct({
|
|
6
8
|
autoMerge: Schema.Boolean,
|
|
@@ -23,10 +25,10 @@ export const State = Schema.Struct({
|
|
|
23
25
|
export type State = typeof State.Type;
|
|
24
26
|
export interface Options {
|
|
25
27
|
readonly apiUrl: string;
|
|
28
|
+
readonly events?: readonly EventKind[];
|
|
26
29
|
readonly fleet: boolean;
|
|
27
30
|
readonly interval: number;
|
|
28
31
|
readonly numbers: readonly number[];
|
|
29
32
|
readonly repository: string;
|
|
30
33
|
readonly stateFile: string;
|
|
31
|
-
readonly token: string;
|
|
32
34
|
}
|
package/src/program.ts
CHANGED
|
@@ -1,34 +1,64 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { argumentsFor, help } from "
|
|
3
|
-
import type
|
|
4
|
-
import {
|
|
1
|
+
import { Effect, Exit } from "effect";
|
|
2
|
+
import { argumentsFor, help } from "#arguments.ts";
|
|
3
|
+
import { type Emit, health, waitForPoll } from "#health.ts";
|
|
4
|
+
import type { WatchError } from "#model.ts";
|
|
5
|
+
import { Watcher } from "#Watcher.ts";
|
|
6
|
+
|
|
5
7
|
export const program = Effect.fn("PrWatch.program")(function* (
|
|
6
8
|
args: readonly string[],
|
|
7
|
-
|
|
8
|
-
emit: (line: string) => Effect.Effect<void, WatchError>,
|
|
9
|
+
emit: Emit,
|
|
9
10
|
diagnostic: (message: string) => Effect.Effect<void, WatchError>,
|
|
10
11
|
) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
let reported: string | undefined;
|
|
13
|
+
let watching = true;
|
|
14
|
+
const run = Effect.gen(function* () {
|
|
15
|
+
const parsed = yield* argumentsFor(args);
|
|
16
|
+
if (parsed.help) {
|
|
17
|
+
watching = false;
|
|
18
|
+
yield* emit(help);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const watcher = yield* Watcher;
|
|
22
|
+
let state = yield* watcher.load(parsed.options);
|
|
23
|
+
function report(message: string) {
|
|
24
|
+
return health(emit, "error", { message }).pipe(
|
|
25
|
+
Effect.tap(
|
|
26
|
+
Effect.sync(() => {
|
|
27
|
+
reported = message;
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
Effect.andThen(diagnostic(message)),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
while (true) {
|
|
34
|
+
reported = undefined;
|
|
35
|
+
state = yield* watcher.poll(parsed.options, state, emit, report);
|
|
23
36
|
if (parsed.once) {
|
|
24
|
-
return
|
|
37
|
+
return;
|
|
25
38
|
}
|
|
26
|
-
yield*
|
|
27
|
-
state = yield* watcher.load(parsed.options);
|
|
39
|
+
yield* waitForPoll(emit, parsed.options.interval, yield* watcher.delay(parsed.options.interval));
|
|
28
40
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
});
|
|
42
|
+
yield* run.pipe(
|
|
43
|
+
Effect.tapError((error) =>
|
|
44
|
+
Effect.gen(function* () {
|
|
45
|
+
if (reported !== error.message) {
|
|
46
|
+
yield* health(emit, "error", { message: error.message });
|
|
47
|
+
}
|
|
48
|
+
if (error.retryAt !== undefined) {
|
|
49
|
+
yield* health(emit, "cooldown", { message: error.message, nextPollAt: new Date(error.retryAt).toISOString() });
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
Effect.onExit((exit) => {
|
|
54
|
+
if (!watching) {
|
|
55
|
+
return Effect.void;
|
|
56
|
+
}
|
|
57
|
+
let reason = "completed";
|
|
58
|
+
if (exit._tag === "Failure") {
|
|
59
|
+
reason = Exit.hasInterrupts(exit) ? "interrupted" : "failed";
|
|
60
|
+
}
|
|
61
|
+
return health(emit, "stopped", { reason }).pipe(Effect.orDie);
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
34
64
|
});
|
package/src/quota.ts
CHANGED
|
@@ -1,42 +1,21 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DateTime } from "effect";
|
|
2
|
+
import type { RateLimitObservation } from "@shivaedev/effect-github/rateLimits.ts";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
limit: number;
|
|
6
|
-
remaining: number;
|
|
7
|
-
reset: number;
|
|
4
|
+
export function quotaRetryAt(observations: readonly RateLimitObservation[], retryAt = 0): number {
|
|
5
|
+
return Math.max(retryAt, ...observations.filter((budget) => budget.remaining === 0).map((budget) => DateTime.toEpochMillis(budget.reset)));
|
|
8
6
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
if (budget.remaining === 0) {
|
|
23
|
-
budget.blockedUntil = Math.max(budget.blockedUntil, budget.reset);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
export function pollDelay(budget: Budget, interval: number, requests: number, now: number): number {
|
|
27
|
-
const ratio = budget.remaining / Math.max(1, budget.limit);
|
|
28
|
-
const multiplier = slowdown(ratio);
|
|
29
|
-
const paced = budget.remaining > 0 && ratio < 0.5 ? Math.ceil((Math.max(0, budget.reset - now) * requests) / budget.remaining) : 0;
|
|
30
|
-
return Math.max(interval * multiplier, paced, budget.blockedUntil - now);
|
|
31
|
-
}
|
|
32
|
-
function retryUntil(retry: string | null, now: number): number {
|
|
33
|
-
if (retry === null) {
|
|
34
|
-
return now + 60_000;
|
|
35
|
-
}
|
|
36
|
-
if (Number.isFinite(Number(retry))) {
|
|
37
|
-
return now + Number(retry) * 1000;
|
|
38
|
-
}
|
|
39
|
-
return Date.parse(retry);
|
|
7
|
+
|
|
8
|
+
export function pollDelay(observations: readonly RateLimitObservation[], interval: number, requests: number, now: number, retryAt = 0): number {
|
|
9
|
+
return Math.max(
|
|
10
|
+
interval,
|
|
11
|
+
quotaRetryAt(observations, retryAt) - now,
|
|
12
|
+
...observations.map((budget) => {
|
|
13
|
+
const ratio = budget.remaining / Math.max(1, budget.limit);
|
|
14
|
+
const reset = DateTime.toEpochMillis(budget.reset);
|
|
15
|
+
const paced = budget.remaining > 0 && ratio < 0.5 ? Math.ceil((Math.max(0, reset - now) * requests) / budget.remaining) : 0;
|
|
16
|
+
return Math.max(interval * slowdown(ratio), paced);
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
40
19
|
}
|
|
41
20
|
function slowdown(ratio: number): number {
|
|
42
21
|
if (ratio < 0.1) {
|
package/dist/httpResponse.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { type HttpClient, type HttpClientResponse } from "effect/http";
|
|
3
|
-
import { WatchError } from "./model.ts";
|
|
4
|
-
export interface CachedResponse {
|
|
5
|
-
readonly body: unknown;
|
|
6
|
-
readonly etag: string | null;
|
|
7
|
-
readonly next: string | undefined;
|
|
8
|
-
}
|
|
9
|
-
export declare function fetchResponse(client: HttpClient.HttpClient, url: string, token: string, cached: CachedResponse | undefined, body?: unknown): Effect.Effect<HttpClientResponse.HttpClientResponse, import("effect/http/HttpBody").HttpBodyError | import("effect/http/HttpClientError").HttpClientError, never>;
|
|
10
|
-
export declare function decodeResponse(url: string, response: HttpClientResponse.HttpClientResponse): Effect.Effect<{
|
|
11
|
-
body: import("effect/Schema").Json;
|
|
12
|
-
etag: string | null;
|
|
13
|
-
next: string | undefined;
|
|
14
|
-
}, WatchError, never>;
|
|
15
|
-
//# sourceMappingURL=httpResponse.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"httpResponse.d.ts","sourceRoot":"","sources":["../src/httpResponse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,KAAK,UAAU,EAAqB,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AACD,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,qKAW1I;AACD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,CAAC,kBAAkB;;;;sBAgB1F"}
|
package/dist/httpResponse.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { HttpClientRequest } from "effect/http";
|
|
3
|
-
import { WatchError } from "./model.js";
|
|
4
|
-
export function fetchResponse(client, url, token, cached, body) {
|
|
5
|
-
const headers = { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" };
|
|
6
|
-
if (token) {
|
|
7
|
-
headers.Authorization = `Bearer ${token}`;
|
|
8
|
-
}
|
|
9
|
-
if (cached?.etag) {
|
|
10
|
-
headers["If-None-Match"] = cached.etag;
|
|
11
|
-
}
|
|
12
|
-
return body === undefined
|
|
13
|
-
? client.get(url, { headers })
|
|
14
|
-
: HttpClientRequest.post(url, { headers }).pipe(HttpClientRequest.bodyJson(body), Effect.flatMap(client.execute));
|
|
15
|
-
}
|
|
16
|
-
export function decodeResponse(url, response) {
|
|
17
|
-
return Effect.gen(function* () {
|
|
18
|
-
if (response.status < 200 || response.status >= 300) {
|
|
19
|
-
return yield* Effect.fail(new WatchError({ message: `GitHub HTTP ${response.status} for ${new URL(url).pathname}` }));
|
|
20
|
-
}
|
|
21
|
-
const body = yield* response.json;
|
|
22
|
-
const next = response.headers.link?.match(/<(?<url>[^>]+)>; rel="next"/u)?.groups?.url;
|
|
23
|
-
yield* Effect.try(() => {
|
|
24
|
-
if (next !== undefined && new URL(next).origin !== new URL(url).origin) {
|
|
25
|
-
throw new Error("Cross-origin pagination refused");
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
return { body, etag: response.headers.etag ?? null, next };
|
|
29
|
-
}).pipe(Effect.mapError((cause) => (cause instanceof WatchError ? cause : new WatchError({ message: "Invalid GitHub response or pagination URL" }))));
|
|
30
|
-
}
|
|
31
|
-
//# sourceMappingURL=httpResponse.js.map
|
package/dist/httpResponse.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"httpResponse.js","sourceRoot":"","sources":["../src/httpResponse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAmB,iBAAiB,EAA2B,MAAM,aAAa,CAAC;AAC1F,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAMxC,MAAM,UAAU,aAAa,CAAC,MAA6B,EAAE,GAAW,EAAE,KAAa,EAAE,MAAkC,EAAE,IAAc;IAC1I,MAAM,OAAO,GAA2B,EAAE,MAAM,EAAE,6BAA6B,EAAE,sBAAsB,EAAE,YAAY,EAAE,CAAC;IACxH,IAAI,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,aAAa,GAAG,UAAU,KAAK,EAAE,CAAC;IAC3C,CAAC;IACD,IAAI,MAAM,EAAE,IAAI,EAAE,CAAC;QAClB,OAAO,CAAC,eAAe,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;IACxC,CAAC;IACD,OAAO,IAAI,KAAK,SAAS;QACxB,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC;QAC9B,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;AACpH,CAAC;AACD,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,QAA+C;IAC1F,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC1B,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YACrD,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,EAAE,OAAO,EAAE,eAAe,QAAQ,CAAC,MAAM,QAAQ,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC;QACvH,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAClC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,8BAA8B,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC;QACvF,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE;YACtB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;gBACxE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;YACpD,CAAC;QACF,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,EAAE,IAAI,EAA2B,CAAC;IACrF,CAAC,CAAC,CAAC,IAAI,CACN,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,EAAE,OAAO,EAAE,2CAA2C,EAAE,CAAC,CAAC,CAAC,CAC5I,CAAC;AACH,CAAC"}
|
package/src/httpResponse.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { type HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/http";
|
|
3
|
-
import { WatchError } from "./model.ts";
|
|
4
|
-
export interface CachedResponse {
|
|
5
|
-
readonly body: unknown;
|
|
6
|
-
readonly etag: string | null;
|
|
7
|
-
readonly next: string | undefined;
|
|
8
|
-
}
|
|
9
|
-
export function fetchResponse(client: HttpClient.HttpClient, url: string, token: string, cached: CachedResponse | undefined, body?: unknown) {
|
|
10
|
-
const headers: Record<string, string> = { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" };
|
|
11
|
-
if (token) {
|
|
12
|
-
headers.Authorization = `Bearer ${token}`;
|
|
13
|
-
}
|
|
14
|
-
if (cached?.etag) {
|
|
15
|
-
headers["If-None-Match"] = cached.etag;
|
|
16
|
-
}
|
|
17
|
-
return body === undefined
|
|
18
|
-
? client.get(url, { headers })
|
|
19
|
-
: HttpClientRequest.post(url, { headers }).pipe(HttpClientRequest.bodyJson(body), Effect.flatMap(client.execute));
|
|
20
|
-
}
|
|
21
|
-
export function decodeResponse(url: string, response: HttpClientResponse.HttpClientResponse) {
|
|
22
|
-
return Effect.gen(function* () {
|
|
23
|
-
if (response.status < 200 || response.status >= 300) {
|
|
24
|
-
return yield* Effect.fail(new WatchError({ message: `GitHub HTTP ${response.status} for ${new URL(url).pathname}` }));
|
|
25
|
-
}
|
|
26
|
-
const body = yield* response.json;
|
|
27
|
-
const next = response.headers.link?.match(/<(?<url>[^>]+)>; rel="next"/u)?.groups?.url;
|
|
28
|
-
yield* Effect.try(() => {
|
|
29
|
-
if (next !== undefined && new URL(next).origin !== new URL(url).origin) {
|
|
30
|
-
throw new Error("Cross-origin pagination refused");
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
return { body, etag: response.headers.etag ?? null, next } satisfies CachedResponse;
|
|
34
|
-
}).pipe(
|
|
35
|
-
Effect.mapError((cause) => (cause instanceof WatchError ? cause : new WatchError({ message: "Invalid GitHub response or pagination URL" }))),
|
|
36
|
-
);
|
|
37
|
-
}
|