╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │350 var moment = try DB.HashMap(.read_write).init(cursor.*); │ │351 var repo_moment = try ctx.repo.core.latestMoment(); │ │352 const read_state = rp.Repo(repo_kind, repo_opts).State(.read_only){ .core = &ctx.repo.core, .extra = .{ .moment = &repo_moment } }; │ │258 - try consumeInTransaction(repo_kind, repo_opts, read_state, ctx.db, &moment, ctx.io, ctx.allocator, ctx.ref); │ │353 + if (!try consumeInTransaction(repo_kind, repo_opts, read_state, ctx.db, &moment, ctx.io, ctx.allocator, ctx.ref)) return error.CancelTransaction;│ │354 } │ │355 }; │ │356 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │357 const history = try DB.ArrayList(.read_write).init(self.db.rootCursor());│ │263 - try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ │ │358 + history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ │ │359 .repo = repo, │ │360 .db = self.db, │ │361 .io = io, │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────╮ │362 .allocator = allocator, │ │363 .ref = ref, │ │269 - }); │ │364 + }) catch |err| switch (err) { │ │365 + error.CancelTransaction => {},│ │366 + else => |e| return e, │ │367 + }; │ │368 } │ │369 }; │ │370 } │ ╰───────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │371 │ │274 -// consume the events on `ref` into the db `moment` writes to. events are read│ │275 -// from `read_state`'s repo, which needn't be the repo backing the db — that's│ │276 -// how a local (possibly git-backed) repo's events land in a standalone db. │ │372 +// consume the events on `ref` into the db `moment` writes to, returning false│ │373 +// if there was nothing to do. events are read from `read_state`'s repo, which│ │374 +// needn't be the repo backing the db — that's how a local (possibly │ │375 +// git-backed) repo's events land in a standalone db. │ │376 pub fn consumeInTransaction( │ │377 comptime repo_kind: rp.RepoKind, │ │378 comptime repo_opts: rp.RepoOpts(repo_kind), │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │382 io: std.Io, │ │383 allocator: std.mem.Allocator, │ │384 ref: rf.Ref, │ │286 -) !void { │ │385 +) !bool { │ │386 const DB = EventDB(repo_opts.hash); │ │387 │ │388 // the last_object_id represents the object id that was last consumed│ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │393 last_object_id_maybe = last_object_id_buffer; │ │394 } │ │395 │ │396 + // the tip was already consumed, so there is nothing to do │ │397 + if (last_object_id_maybe) |*last_object_id| { │ │398 + const tip_hex = (try rf.readRecur(repo_kind, repo_opts, read_state, io, .{ .ref = ref })) orelse return error.OidNotFound;│ │399 + var tip: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │400 + _ = try std.fmt.hexToBytes(&tip, &tip_hex); │ │401 + if (std.mem.eql(u8, last_object_id, &tip)) return false; │ │402 + } │ │403 + │ │404 // the list with all of haxy's state, including materialized views. │ │405 // the reason it is a list is so we can keep every previous haxy │ │406 // state, making it easy to revert to an older state if the user │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │439 │ │440 try haxy.slice(moment_index + 1); │ │441 try moment.put(hash.hashInt(repo_opts.hash, "haxy-last-object-id"), .{ .bytes = &head_oid });│ │335 - return; │ │442 + return true; │ │443 } │ │444 │ │445 // if this branch was rebased and force pushed, we need to detect that and │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │595 if (last_object_id_maybe) |*last_object_id| { │ │596 try moment.put(hash.hashInt(repo_opts.hash, "haxy-last-object-id"), .{ .bytes = last_object_id });│ │597 } │ │598 + │ │599 + return true; │ │600 } │ │601 │ │602 // the conflicted records of one kind, keyed by the same order key its id set │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │956 return try DB.HashMap(.read_only).init(haxy_moment_cursor); │ │957 } │ │958 │ │850 -// commit each event as a JSON commit message on `ref`, then consume them │ │851 -pub fn commitAndConsume( │ │852 - comptime repo_kind: rp.RepoKind, │ │853 - comptime repo_opts: rp.RepoOpts(repo_kind), │ │854 - io: std.Io, │ │855 - allocator: std.mem.Allocator, │ │856 - repo: *rp.Repo(repo_kind, repo_opts), │ │857 - ref: rf.Ref, │ │858 - events: []const EventWithId, │ │859 -) !void { │ │860 - // a branch that only exists on a remote continues from the remote tip, │ │861 - // so a cloned repo's local branch stays connected to the server history │ │862 - const first_parent_oids: ?[1][hash.hexLen(repo_opts.hash)]u8 = blk: { │ │863 - if (try repo.readRef(io, ref) != null) break :blk null; │ │864 - var remotes = try repo.listRemotes(io, allocator); │ │865 - defer remotes.deinit(); │ │866 - for (remotes.sections.keys()) |remote_name| { │ │867 - const remote_ref: rf.Ref = .{ .kind = .{ .remote = remote_name }, .name = ref.name }; │ │868 - if (try repo.readRef(io, remote_ref)) |oid| break :blk .{oid}; │ │869 - } │ │870 - break :blk null; │ │871 - }; │ │872 - var parent_oids: ?[]const [hash.hexLen(repo_opts.hash)]u8 = if (first_parent_oids) |*oids| oids else null; │ │873 - │ │874 - var json: std.Io.Writer.Allocating = .init(allocator); │ │875 - defer json.deinit(); │ │876 - │ │877 - for (events) |event| { │ │878 - json.clearRetainingCapacity(); │ │879 - try std.json.Stringify.value(event, .{ .whitespace = .indent_2 }, &json.writer); │ │880 - _ = try repo.commitAtRef(io, allocator, .{ .author = "haxy <user@haxy>", .message = json.written(), .timestamp = event.timestamp, .parent_oids = parent_oids }, null, ref);│ │881 - // later events parent on the ref's new tip │ │882 - parent_oids = null; │ │883 - } │ │884 - │ │885 - try consume(repo_kind, repo_opts, io, allocator, repo, ref); │ │886 -} │ │887 - │ │959 // build the key for SortedSets sorted by timestamp. the big-endian timestamp │ │960 // makes byte order match creation order; the event id breaks ties and keeps │ │961 // keys unique within the same timestamp. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1032 io.random(&id_bytes); │ │1033 const event_id_hex = std.fmt.bytesToHex(id_bytes, .lower); │ │1034 │ │ 964 - try commitAndConsume(.xit, admin_repo_opts, io, allocator, &repo, events_ref, &[_]EventWithId{.{│ │1035 + try consume(.xit, admin_repo_opts, io, allocator, &repo, events_ref, &[_]EventWithId{.{ │ │1036 .id = event_id_hex, │ │1037 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │1038 .event = .{ .repo = .{ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯