feat: add RemoteExport and deinit

This commit is contained in:
dualfroz committed 2026-09-01 20:32:23 +02:00
1 parent a5ba9ed3db
commit cc546fe216
1 file changed
+158
+158
View File
@@ -0,0 +1,158 @@
const std = @import("std");
const protocol = @import("protocol.zig");
const dbmod = @import("db.zig");
const rowmod = @import("row.zig");
const authmod = @import("auth.zig");
const ttlmod = @import("ttl.zig");
const collmod = @import("collections.zig");
const aggregatemod = @import("aggregate.zig");
const windowmod = @import("window.zig");
const querymod = @import("query.zig");
const Allocator = std.mem.Allocator;
pub const ClientError = error{
ServerBadRequest,
ServerInternal,
ServerConflict,
ServerRateLimited,
ServerUnauthorized,
ServerForbidden,
ServerUserNotFound,
ServerKeyNotFound,
ServerCollectionNotFound,
UnexpectedStatus,
};
/// Result of a remote `table export`: the row count `dump.zig`'s exporter
/// reported plus the rendered bytes, independently allocated so they
/// outlive the response frame they arrived in.
pub const RemoteExport = struct {
rowCount: u64,
content: []u8,
pub fn deinit(self: RemoteExport, allocator: Allocator) void {
allocator.free(self.content);
}
};
fn statusToError(status: protocol.Status) ClientError {
return switch (status) {
.bad_request => error.ServerBadRequest,
.internal => error.ServerInternal,
.conflict => error.ServerConflict,
.rate_limited => error.ServerRateLimited,
.unauthorized => error.ServerUnauthorized,
.forbidden => error.ServerForbidden,
else => error.UnexpectedStatus,
};
}
/// A blocking client for the wire protocol in `protocol.zig`. Each call
/// writes one request frame and reads exactly one response frame, so a
/// `Client` must not be shared across threads without external locking.
pub const Client = struct {
allocator: Allocator,
stream: std.net.Stream,
/// The session token from the last successful `authenticate` call, if
/// the server had `enableSessions` on (see server.zig) - null on a
/// server without sessions, or before any successful `authenticate`.
/// Owned by the `Client`, freed on `close()` and on every subsequent
/// `authenticate`/`authenticateWithToken` call.
token: ?[]u8 = null,
pub fn connect(allocator: Allocator, address: std.net.Address) !Client {
const stream = try std.net.tcpConnectToAddress(address);
return .{ .allocator = allocator, .stream = stream };
}
pub fn close(self: *Client) void {
if (self.token) |t| self.allocator.free(t);
self.stream.close();
}
fn setToken(self: *Client, payload: []const u8) !void {
if (self.token) |old| self.allocator.free(old);
self.token = if (payload.len > 0) try self.allocator.dupe(u8, payload) else null;
}
/// The session token captured by the last successful `authenticate`,
/// if any (see `token` above). Callers that want to reconnect later
/// without a password should dupe this before the `Client` closes.
pub fn sessionToken(self: *Client) ?[]const u8 {
return self.token;
}
fn roundTrip(self: *Client, op: protocol.Op, payload: []const u8) !protocol.Frame {
try protocol.writeFrame(self.stream.writer(), @intFromEnum(op), payload);
return protocol.readFrame(self.allocator, self.stream.reader());
}
/// Authenticates this connection against a server that has auth
/// enabled (see `server.zig`'s `enableAuth`). Every request on this
/// stream after a successful call is dispatched under the role
/// returned by the server's `auth.Store.verify`; a server without auth
/// enabled always answers `ok` regardless of the credentials given. If
/// the server also has sessions enabled (`enableSessions`), the
/// response carries a fresh token captured into `sessionToken()` -
/// otherwise `sessionToken()` reads null afterwards, same as before
/// this call was ever made.
pub fn authenticate(self: *Client, username: []const u8, password: []const u8) !void {
const payload = try protocol.encodeAuth(self.allocator, username, password);
defer self.allocator.free(payload);
var frame = try self.roundTrip(.auth, payload);
defer frame.deinit();
const status = try protocol.Status.fromByte(frame.tag);
if (status != .ok) return statusToError(status);
try self.setToken(frame.payload);
}
/// Authenticates this connection using a token obtained from another
/// connection's `sessionToken()`, instead of a username/password pair
/// (see server.zig's `auth_token` opcode) - skips the password
/// entirely and resumes the same role until the session expires. A
/// server without sessions enabled accepts any token unconditionally,
/// matching `authenticate`'s behavior against a server without auth.
pub fn authenticateWithToken(self: *Client, token: []const u8) !void {
const payload = try protocol.encodeAuthToken(self.allocator, token);
defer self.allocator.free(payload);
var frame = try self.roundTrip(.auth_token, payload);
defer frame.deinit();
const status = try protocol.Status.fromByte(frame.tag);
if (status != .ok) return statusToError(status);
}
pub fn ping(self: *Client) !void {
var frame = try self.roundTrip(.ping, "");
defer frame.deinit();
const status = try protocol.Status.fromByte(frame.tag);
if (status != .ok) return statusToError(status);
}
pub fn get(self: *Client, allocator: Allocator, key: []const u8) !?[]u8 {
var frame = try self.roundTrip(.get, key);
defer frame.deinit();
const status = try protocol.Status.fromByte(frame.tag);
return switch (status) {
.ok => try allocator.dupe(u8, frame.payload),
.not_found => null,
else => statusToError(status),
};
}
pub fn set(self: *Client, key: []const u8, value: []const u8) !void {
const payload = try protocol.encodeSet(self.allocator, key, value);
defer self.allocator.free(payload);
var frame = try self.roundTrip(.set, payload);
defer frame.deinit();
const status = try protocol.Status.fromByte(frame.tag);
if (status != .ok) return statusToError(status);
}
pub fn delete(self: *Client, key: []const u8) !void {
var frame = try self.roundTrip(.del, key);
defer frame.deinit();
const status = try protocol.Status.fromByte(frame.tag);
if (status != .ok) return statusToError(status);
}