-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathgithub-releases.ts
More file actions
97 lines (84 loc) · 2.78 KB
/
Copy pathgithub-releases.ts
File metadata and controls
97 lines (84 loc) · 2.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import { Clock, Context, Effect, Layer, Ref, Result, Schema } from "effect";
import { githubReleasesApiResponse, type ListReleasesOutput } from "./schemas";
const RELEASES_URL =
"https://api.github.com/repos/PostHog/code/releases?per_page=30";
const CACHE_TTL_MS = 10 * 60_000;
const FETCH_TIMEOUT_MS = 10_000;
export class GitHubReleasesError extends Schema.TaggedErrorClass<GitHubReleasesError>()(
"GitHubReleasesError",
{ cause: Schema.Defect() },
) {}
interface CachedReleases {
readonly fetchedAt: number;
readonly data: ListReleasesOutput;
}
const fetchReleases = Effect.fn("GitHubReleases.fetch")(function* () {
const response = yield* Effect.tryPromise({
try: () =>
fetch(RELEASES_URL, {
headers: { Accept: "application/vnd.github+json" },
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
}),
catch: (cause) => new GitHubReleasesError({ cause }),
});
if (!response.ok) {
return yield* Effect.fail(
new GitHubReleasesError({
cause: new Error(`GitHub releases fetch failed: ${response.status}`),
}),
);
}
const json = yield* Effect.tryPromise({
try: () => response.json(),
catch: (cause) => new GitHubReleasesError({ cause }),
});
const parsed = yield* Effect.try({
try: () => githubReleasesApiResponse.parse(json),
catch: (cause) => new GitHubReleasesError({ cause }),
});
const releases = parsed
.filter((release) => !release.draft)
.map((release) => ({
version: release.tag_name.replace(/^v/, ""),
name:
release.name && release.name.length > 0
? release.name
: release.tag_name,
notes: release.body ?? "",
date: release.published_at,
isPrerelease: release.prerelease,
htmlUrl: release.html_url,
}));
return { releases } satisfies ListReleasesOutput;
});
type ReleasesCache = Ref.Ref<CachedReleases | null>;
const listReleases = Effect.fn("GitHubReleases.list")(function* (
cache: ReleasesCache,
) {
const now = yield* Clock.currentTimeMillis;
const cached = yield* Ref.get(cache);
if (cached && now - cached.fetchedAt < CACHE_TTL_MS) {
return cached.data;
}
const result = yield* Effect.result(fetchReleases());
if (Result.isSuccess(result)) {
yield* Ref.set(cache, { fetchedAt: now, data: result.success });
return result.success;
}
// The refresh failed: serve the last good copy if we have one, else fail.
if (cached) {
return cached.data;
}
return yield* Effect.fail(result.failure);
});
export class GitHubReleases extends Context.Service<GitHubReleases>()(
"GitHubReleases",
{
make: Effect.gen(function* () {
const cache = yield* Ref.make<CachedReleases | null>(null);
return { list: () => listReleases(cache) };
}),
},
) {
static Live = Layer.effect(this, this.make);
}