tangled
alpha
login
or
join now
altagos.dev
/
space
0
fork
atom
A SpaceTraders Agent
0
fork
atom
overview
issues
pulls
pipelines
create rate-limited http client for spacetraders
altagos.dev
4 months ago
e513358c
b6f5095b
verified
This commit was signed with the committer's
known signature
.
altagos.dev
SSH Key Fingerprint:
SHA256:UbTjEcCZlc6GzQWLCuDK3D//HESWD2xFPkzue9XMras=
+302
1 changed file
expand all
collapse all
unified
split
src
st
http.zig
+302
src/st/http.zig
···
1
1
+
const std = @import("std");
2
2
+
const HTTPClient = std.http.Client;
3
3
+
const Io = std.Io;
4
4
+
const json = std.json;
5
5
+
6
6
+
const models = @import("models.zig");
7
7
+
8
8
+
const TIME_SLEEP_FACTOR: f64 = 0.98;
9
9
+
10
10
+
const log = std.log.scoped(.SpaceTraders);
11
11
+
12
12
+
const Semaphore = struct {
13
13
+
mutex: Io.Mutex = .{ .state = .unlocked },
14
14
+
cond: Io.Condition = .{},
15
15
+
/// It is OK to initialise this field to any value.
16
16
+
permits: u64 = 0,
17
17
+
18
18
+
pub fn post(sem: *Semaphore, io: Io) void {
19
19
+
sem.mutex.lockUncancelable(io);
20
20
+
defer sem.mutex.unlock(io);
21
21
+
22
22
+
sem.permits += 1;
23
23
+
sem.cond.signal(io);
24
24
+
}
25
25
+
26
26
+
pub fn set(sem: *Semaphore, io: Io, permits: u64) void {
27
27
+
sem.mutex.lockUncancelable(io);
28
28
+
defer sem.mutex.unlock(io);
29
29
+
30
30
+
sem.permits = permits;
31
31
+
sem.cond.signal(io);
32
32
+
}
33
33
+
34
34
+
pub fn wait(sem: *Semaphore, io: Io) !void {
35
35
+
sem.mutex.lockUncancelable(io);
36
36
+
defer sem.mutex.unlock(io);
37
37
+
38
38
+
while (sem.permits == 0)
39
39
+
try sem.cond.wait(io, &sem.mutex);
40
40
+
41
41
+
sem.permits -= 1;
42
42
+
if (sem.permits > 0)
43
43
+
sem.cond.signal(io);
44
44
+
}
45
45
+
46
46
+
pub fn available(sem: *Semaphore, io: Io) u64 {
47
47
+
sem.mutex.lockUncancelable(io);
48
48
+
defer sem.mutex.unlock(io);
49
49
+
50
50
+
return sem.permits;
51
51
+
}
52
52
+
};
53
53
+
54
54
+
pub const Limiter = struct {
55
55
+
points: u64,
56
56
+
duration: i64,
57
57
+
time: ?Io.Timestamp,
58
58
+
59
59
+
mutex: Io.Mutex = .{ .state = .unlocked },
60
60
+
semaphor: Semaphore,
61
61
+
62
62
+
pub fn init(opts: struct { points: u54 = 2, duration: i64 = 1000 }) Limiter {
63
63
+
return .{
64
64
+
.points = opts.points,
65
65
+
.duration = opts.duration,
66
66
+
.time = null,
67
67
+
.semaphor = Semaphore{ .permits = opts.points },
68
68
+
};
69
69
+
}
70
70
+
71
71
+
pub fn checkReset(l: *Limiter, io: Io) bool {
72
72
+
l.mutex.lock(io) catch return false;
73
73
+
defer l.mutex.unlock(io);
74
74
+
75
75
+
if (l.time) |t| {
76
76
+
const dur = t.durationTo(Io.Clock.now(.real, io) catch return false);
77
77
+
if (dur.toSeconds() > 0) {
78
78
+
l.semaphor.set(io, l.points);
79
79
+
l.time = null;
80
80
+
return true;
81
81
+
}
82
82
+
}
83
83
+
84
84
+
return false;
85
85
+
}
86
86
+
87
87
+
pub fn aquire(l: *Limiter, io: Io) !void {
88
88
+
try l.mutex.lock(io);
89
89
+
defer l.mutex.unlock(io);
90
90
+
91
91
+
if (l.time == null) {
92
92
+
const now = try Io.Clock.now(.real, io);
93
93
+
l.time = now.addDuration(.fromMilliseconds(l.duration));
94
94
+
}
95
95
+
96
96
+
return l.semaphor.wait(io);
97
97
+
}
98
98
+
99
99
+
pub fn timeToReset(l: *Limiter, io: Io) i64 {
100
100
+
if (l.time) |t| {
101
101
+
return t.durationTo(Io.Clock.now(.real, io) catch return 0).raw.toMilliseconds();
102
102
+
}
103
103
+
return 0;
104
104
+
}
105
105
+
106
106
+
pub fn available(l: *Limiter, io: Io) bool {
107
107
+
return l.semaphor.available(io) > 0;
108
108
+
}
109
109
+
};
110
110
+
111
111
+
pub const BurstyLimiter = struct {
112
112
+
static: Limiter,
113
113
+
burst: Limiter,
114
114
+
115
115
+
pub fn wait(bl: *BurstyLimiter, io: Io) !bool {
116
116
+
_ = bl.static.checkReset(io);
117
117
+
_ = bl.burst.checkReset(io);
118
118
+
119
119
+
if (!bl.static.available(io)) {
120
120
+
if (bl.burst.available(io)) {
121
121
+
log.debug("Using Burst", .{});
122
122
+
try bl.burst.aquire(io);
123
123
+
return true;
124
124
+
} else {
125
125
+
log.warn("No request available, waiting", .{});
126
126
+
}
127
127
+
}
128
128
+
129
129
+
try bl.static.aquire(io);
130
130
+
return true;
131
131
+
}
132
132
+
};
133
133
+
134
134
+
pub const AuthType = enum { account, agent, none };
135
135
+
136
136
+
pub const Auth = struct {
137
137
+
account: []const u8 = "",
138
138
+
agent: []const u8 = "",
139
139
+
};
140
140
+
141
141
+
pub const RequestOptions = struct {
142
142
+
method: std.http.Method = .GET,
143
143
+
auth: AuthType = .none,
144
144
+
body: Body = .empty,
145
145
+
146
146
+
pub const Body = union(enum) {
147
147
+
empty: void,
148
148
+
buffer: []u8,
149
149
+
};
150
150
+
151
151
+
pub fn authorization(opts: *const RequestOptions, client: *const Client) HTTPClient.Request.Headers.Value {
152
152
+
switch (opts.auth) {
153
153
+
.account => return .{ .override = client.auth.account },
154
154
+
.agent => return .{ .override = client.auth.agent },
155
155
+
.none => return .{ .omit = {} },
156
156
+
}
157
157
+
}
158
158
+
};
159
159
+
160
160
+
pub const RequestError = error{
161
161
+
OutOfMemory,
162
162
+
InvalidResponse,
163
163
+
RateLimiterError,
164
164
+
} || HTTPClient.RequestError || HTTPClient.Request.ReceiveHeadError || std.Uri.ParseError;
165
165
+
166
166
+
pub fn RawResponse(comptime T: type) type {
167
167
+
return Io.Future(RequestError!json.Parsed(T));
168
168
+
}
169
169
+
170
170
+
pub fn Response(comptime T: type) type {
171
171
+
return RawResponse(models.Wrapper(T));
172
172
+
}
173
173
+
174
174
+
pub const Client = struct {
175
175
+
allocator: std.mem.Allocator,
176
176
+
io: Io,
177
177
+
limiter: BurstyLimiter,
178
178
+
179
179
+
base_url: []const u8,
180
180
+
auth: Auth,
181
181
+
182
182
+
http: HTTPClient,
183
183
+
184
184
+
pub fn init(
185
185
+
allocator: std.mem.Allocator,
186
186
+
io: std.Io,
187
187
+
opts: struct {
188
188
+
base_url: []const u8 = "https://api.spacetraders.io/v2",
189
189
+
auth: Auth = .{},
190
190
+
},
191
191
+
) Client {
192
192
+
return .{
193
193
+
.allocator = allocator,
194
194
+
.io = io,
195
195
+
.limiter = .{
196
196
+
.static = .init(.{}),
197
197
+
.burst = .init(.{ .points = 30, .duration = 60_000 }),
198
198
+
},
199
199
+
.base_url = opts.base_url,
200
200
+
.auth = opts.auth,
201
201
+
.http = .{ .allocator = allocator, .io = io },
202
202
+
};
203
203
+
}
204
204
+
205
205
+
pub fn deinit(client: *Client) void {
206
206
+
client.http.deinit();
207
207
+
}
208
208
+
209
209
+
pub fn request(
210
210
+
client: *Client,
211
211
+
comptime T: type,
212
212
+
comptime path: []const u8,
213
213
+
args: anytype,
214
214
+
opts: RequestOptions,
215
215
+
) !RawResponse(T) {
216
216
+
const path_fmt = try std.fmt.allocPrint(client.allocator, path, args);
217
217
+
defer client.allocator.free(path_fmt);
218
218
+
219
219
+
const url = try std.fmt.allocPrint(client.allocator, "{s}{s}", .{ client.base_url, path_fmt });
220
220
+
221
221
+
const Wrapper = struct {
222
222
+
fn call(
223
223
+
cl: *Client,
224
224
+
url_param: []const u8,
225
225
+
opts_param: *const RequestOptions,
226
226
+
) RequestError!json.Parsed(T) {
227
227
+
defer cl.allocator.free(url_param);
228
228
+
if (cl.limiter.wait(cl.io) catch return error.RateLimiterError)
229
229
+
return Client.requestRaw(cl, T, url_param, opts_param);
230
230
+
return error.RateLimiterError;
231
231
+
}
232
232
+
};
233
233
+
234
234
+
return client.io.concurrent(
235
235
+
Wrapper.call,
236
236
+
.{ client, url, &opts },
237
237
+
);
238
238
+
}
239
239
+
240
240
+
pub fn requestRaw(
241
241
+
client: *Client,
242
242
+
comptime T: type,
243
243
+
url: []const u8,
244
244
+
opts: *const RequestOptions,
245
245
+
) RequestError!json.Parsed(T) {
246
246
+
const uri = std.Uri.parse(url) catch |err| {
247
247
+
log.err("Error parsing url: {} - url = {s}", .{ err, url });
248
248
+
return err;
249
249
+
};
250
250
+
251
251
+
var req = try client.http.request(opts.method, uri, .{
252
252
+
.headers = .{
253
253
+
.authorization = opts.authorization(client),
254
254
+
.user_agent = .{ .override = "All your codebases are belong to us" },
255
255
+
},
256
256
+
});
257
257
+
defer req.deinit();
258
258
+
259
259
+
log.debug("requesting: {s}", .{uri.path.percent_encoded});
260
260
+
261
261
+
switch (opts.body) {
262
262
+
.empty => try req.sendBodiless(),
263
263
+
.buffer => |body| try req.sendBodyComplete(body),
264
264
+
}
265
265
+
266
266
+
var redirect_buffer: [1024]u8 = undefined;
267
267
+
268
268
+
var response = try req.receiveHead(&redirect_buffer);
269
269
+
const colour = blk: {
270
270
+
if (std.mem.eql(u8, response.head.reason, "OK")) {
271
271
+
break :blk "\x1b[92m";
272
272
+
} else {
273
273
+
break :blk "\x1b[1m\x1b[91m";
274
274
+
}
275
275
+
};
276
276
+
log.debug(
277
277
+
"\x1b[2m[path = {s}]\x1b[0m received {s}{d} {s}\x1b[0m",
278
278
+
.{ url[client.base_url.len..], colour, response.head.status, response.head.reason },
279
279
+
);
280
280
+
281
281
+
// var header_iter = response.head.iterateHeaders();
282
282
+
// while (header_iter.next()) |header| {
283
283
+
// log.debug("{s}: {s}", .{ header.name, header.value });
284
284
+
// }
285
285
+
286
286
+
var decompress_buffer: [std.compress.flate.max_window_len]u8 = undefined;
287
287
+
var transfer_buffer: [64]u8 = undefined;
288
288
+
var decompress: std.http.Decompress = undefined;
289
289
+
290
290
+
const decompressed_body_reader = response.readerDecompressing(&transfer_buffer, &decompress, &decompress_buffer);
291
291
+
292
292
+
var json_reader: json.Reader = .init(client.allocator, decompressed_body_reader);
293
293
+
defer json_reader.deinit();
294
294
+
295
295
+
return json.parseFromTokenSource(T, client.allocator, &json_reader, .{
296
296
+
.ignore_unknown_fields = true,
297
297
+
}) catch |err| {
298
298
+
log.err("Error parsing response: {}", .{err});
299
299
+
return RequestError.InvalidResponse;
300
300
+
};
301
301
+
}
302
302
+
};