1import { SourceControlProviderKind } from "@t3tools/contracts";
2import { assert, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as TestClock from "effect/testing/TestClock";
5
6import * as SourceControlRateLimit from "./SourceControlRateLimit.ts";
7
8const github = { provider: SourceControlProviderKind.make("github"), host: "github.com" };
9
10it.effect("isolates cooldowns for verified credentials on the same host", () =>
11 Effect.gen(function* () {
12 const limits = yield* SourceControlRateLimit.SourceControlRateLimit;
13 yield* limits
14 .recordRateLimit({ ...github, lease: 0 })
15 .pipe(Effect.provideService(SourceControlRateLimit.CredentialScope, "first"));
16 yield* limits
17 .check(github)
18 .pipe(Effect.provideService(SourceControlRateLimit.CredentialScope, "second"));
19 const error = yield* limits
20 .check(github)
21 .pipe(Effect.provideService(SourceControlRateLimit.CredentialScope, "first"), Effect.flip);
22 assert.strictEqual(error._tag, "SourceControlRateLimitPausedError");
23 }).pipe(Effect.provide(SourceControlRateLimit.layer)),
24);
25
26it("parses Retry-After seconds and HTTP dates", () => {
27 assert.equal(SourceControlRateLimit.retryAtFromHeader("120", 1_000), 121_000);
28 assert.equal(
29 SourceControlRateLimit.retryAtFromHeader("Thu, 01 Jan 1970 00:02:01 GMT", 1_000),
30 121_000,
31 );
32 assert.isUndefined(SourceControlRateLimit.retryAtFromHeader("later", 1_000));
33});
34
35it.effect("backs off repeated rate limits until a successful request", () =>
36 Effect.gen(function* () {
37 yield* TestClock.setTime(0);
38 const limits = yield* SourceControlRateLimit.SourceControlRateLimit;
39
40 const firstLease = yield* limits.check(github);
41 yield* limits.recordRateLimit({ ...github, lease: firstLease });
42 const firstPause = yield* Effect.flip(limits.check(github));
43 assert.deepInclude(firstPause, {
44 _tag: "SourceControlRateLimitPausedError",
45 provider: SourceControlProviderKind.make("github"),
46 host: "github.com",
47 retryAt: 30_000,
48 });
49 assert.equal(
50 firstPause.detail,
51 "github requests to github.com are paused until the rate limit resets.",
52 );
53 assert.equal(firstPause.message, firstPause.detail);
54
55 yield* TestClock.adjust("30 seconds");
56 const secondLease = yield* limits.check(github);
57 yield* limits.recordRateLimit({ ...github, lease: secondLease });
58 assert.equal((yield* Effect.flip(limits.check(github))).retryAt, 90_000);
59
60 yield* TestClock.adjust("60 seconds");
61 const successLease = yield* limits.check(github);
62 yield* limits.recordSuccess({ ...github, lease: successLease });
63 const resetLease = yield* limits.check(github);
64 yield* limits.recordRateLimit({ ...github, lease: resetLease });
65 assert.equal((yield* Effect.flip(limits.check(github))).retryAt, 120_000);
66 }).pipe(Effect.provide(SourceControlRateLimit.layer)),
67);
68
69it.effect("honors a provider reset time", () =>
70 Effect.gen(function* () {
71 yield* TestClock.setTime(1_000);
72 const limits = yield* SourceControlRateLimit.SourceControlRateLimit;
73 const lease = yield* limits.check(github);
74
75 yield* limits.recordRateLimit({ ...github, lease, retryAt: 121_000 });
76 assert.equal((yield* Effect.flip(limits.check(github))).retryAt, 121_000);
77
78 yield* TestClock.adjust("119999 millis");
79 assert.equal((yield* Effect.flip(limits.check(github))).retryAt, 121_000);
80 yield* TestClock.adjust("1 millis");
81 assert.equal(yield* limits.check(github), 1);
82 }).pipe(Effect.provide(SourceControlRateLimit.layer)),
83);
84
85it.effect("lets an interactive request through without clearing an active pause", () =>
86 Effect.gen(function* () {
87 yield* TestClock.setTime(1_000);
88 const limits = yield* SourceControlRateLimit.SourceControlRateLimit;
89 const lease = yield* limits.check(github);
90 yield* limits.recordRateLimit({ ...github, lease, retryAt: 121_000 });
91
92 const interactiveLease = yield* limits.check(github, { allowPaused: true });
93 yield* limits.recordSuccess({ ...github, lease: interactiveLease });
94
95 assert.equal(interactiveLease, 1);
96 assert.equal((yield* Effect.flip(limits.check(github))).retryAt, 121_000);
97
98 yield* limits.recordRateLimit({ ...github, lease: interactiveLease, retryAt: 61_000 });
99 assert.equal((yield* Effect.flip(limits.check(github))).retryAt, 121_000);
100 }).pipe(Effect.provide(SourceControlRateLimit.layer)),
101);
102
103it.effect("keeps providers and hosts isolated", () =>
104 Effect.gen(function* () {
105 const limits = yield* SourceControlRateLimit.SourceControlRateLimit;
106 const lease = yield* limits.check(github);
107 yield* limits.recordRateLimit({ ...github, lease });
108
109 assert.equal(
110 yield* limits.check({
111 provider: SourceControlProviderKind.make("gitlab"),
112 host: "github.com",
113 }),
114 0,
115 );
116 assert.equal(
117 yield* limits.check({
118 provider: SourceControlProviderKind.make("github"),
119 host: "github.example.com",
120 }),
121 0,
122 );
123 }).pipe(Effect.provide(SourceControlRateLimit.layer)),
124);
125
126it.effect("does not let an older success clear a concurrent pause", () =>
127 Effect.gen(function* () {
128 const limits = yield* SourceControlRateLimit.SourceControlRateLimit;
129 const firstLease = yield* limits.check(github);
130 const concurrentLease = yield* limits.check(github);
131
132 yield* limits.recordRateLimit({ ...github, lease: firstLease });
133 yield* limits.recordSuccess({ ...github, lease: concurrentLease });
134
135 assert.equal((yield* Effect.flip(limits.check(github))).retryAt, 30_000);
136 }).pipe(Effect.provide(SourceControlRateLimit.layer)),
137);
138
139it.effect("keeps a fresh provider reset from an older request", () =>
140 Effect.gen(function* () {
141 yield* TestClock.setTime(0);
142 const limits = yield* SourceControlRateLimit.SourceControlRateLimit;
143 const staleLease = yield* limits.check(github);
144 yield* limits.recordRateLimit({ ...github, lease: staleLease });
145
146 yield* TestClock.adjust("31 seconds");
147 yield* limits.recordRateLimit({ ...github, lease: staleLease, retryAt: 61_000 });
148
149 assert.equal((yield* Effect.flip(limits.check(github))).retryAt, 61_000);
150 }).pipe(Effect.provide(SourceControlRateLimit.layer)),
151);