From cc546fe2165b2c4083d17fbbb52b41784dc48354 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 20:32:23 +0200 Subject: [PATCH] feat: add RemoteExport and deinit --- src/client.zig | 158 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/client.zig diff --git a/src/client.zig b/src/client.zig new file mode 100644 index 0000000..8947e0d --- /dev/null +++ b/src/client.zig @@ -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); + } +