outbox

package
v0.5.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package outbox holds the ticket writes that have not reached the remote yet.

It exists because of a decision about how the tool should feel: claiming a ticket works offline. Being offline is rare enough that it must not dictate the interaction — so a claim is written locally straight away and the push is caught up when there is a network again. Something has to hold the write in between, and that is this.

It also settles a problem that would otherwise sit in the write path. Every ticket write funnels through core/ticket's Mutate and Create, and those are called from the TUI and from the merge driver as well as from the CLI. A synchronous push there would hang the board on the network and would have the merge driver pushing in the middle of a git merge. With an outbox, the write path only files the write; sending happens outside it.

The outbox lives under the per-working-tree state directory, not in the repository: an unsent write is this machine's business, and committing it would publish it in the very moment it is meant to be waiting.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Box

type Box struct{ Dir string }

Box is the queue for one working tree.

func At

func At(s *ticket.Store) *Box

At returns the box for a store's working tree.

func (*Box) Drop

func (b *Box) Drop(id string) error

Drop removes a queued write.

func (*Box) DropEpic added in v0.5.1

func (b *Box) DropEpic(name string) error

DropEpic removes a queued write of an epic.

func (*Box) DropMilestone added in v0.5.0

func (b *Box) DropMilestone(key string) error

DropMilestone removes a queued write of a milestone.

func (*Box) Flush

func (b *Box) Flush(s Sender) ([]Result, error)

Flush sends what it can and reports each entry.

It stops at the first unreachable remote instead of walking the rest of the queue. The network is a property of the machine, not of the ticket: once one push has proved there is no route, every following attempt would wait for the same timeout, and a command the user ran for another reason entirely would sit there for as long as the queue is.

A lost race is the opposite case and does not stop anything: it is about one ticket, and the next one may well go through.

func (*Box) List

func (b *Box) List() ([]Entry, error)

List returns every unsent write, oldest first. Ticket ids are ULIDs, so sorting by id is chronological by creation; QueuedAt orders by when the write was filed, which is what a flush should follow.

func (*Box) Pending

func (b *Box) Pending(id string) (Entry, bool)

Pending returns the unsent write for one ticket, if there is one. The board asks this to mark a card as carrying something not yet sent.

func (*Box) PendingEpic added in v0.5.1

func (b *Box) PendingEpic(name string) (Entry, bool)

PendingEpic returns the unsent write of one epic. It is what an epic is read from while it waits: with no file on disk, the queue holds the only copy of an edit made offline.

func (*Box) PendingMilestone added in v0.5.0

func (b *Box) PendingMilestone(key string) (Entry, bool)

PendingMilestone returns the unsent write for one milestone key, if there is one, in either folder a milestone write is filed in.

func (*Box) Queue

func (b *Box) Queue(id string, op Op, content []byte, lease, by string) error

Queue files a write, superseding any earlier unsent write for the same ticket. Superseding is correct rather than lossy: the entry carries the whole ticket, so the newer bytes already contain everything the older ones said.

func (*Box) QueueEpic added in v0.5.1

func (b *Box) QueueEpic(name string, content []byte, lease, by string) error

QueueEpic files a pending write of an epic, under the same rules.

func (*Box) QueueMigration added in v0.5.0

func (b *Box) QueueMigration(key, file string, migrated []byte, from, by string) error

QueueMigration files the first write of a milestone's ref when it moves an older file to its ID: the migrated content, and the old ref's tip (or "none"), which are sent as a commit of their own. Later edits replace the content and leave the migration as it is.

func (*Box) QueueMilestone added in v0.3.0

func (b *Box) QueueMilestone(key, file string, op Op, content []byte, lease, by string) error

QueueMilestone files a pending write of a milestone file, under the same rules: whole content, the lease the remote last confirmed, superseding any earlier unsent write of the same milestone.

func (*Box) RekeyMilestone added in v0.5.0

func (b *Box) RekeyMilestone(oldKey, newKey, file string, content []byte, lease string) error

RekeyMilestone replaces the write an older build queued under oldKey — a milestone's name — with one under newKey, its ID, naming the file to commit. The new entry keeps when and by whom the old one was queued. If newKey already has a write queued, that one stands: it is newer than anything an old build could have said, and the old entry is dropped.

type Entry

type Entry struct {
	ID  string `json:"id"`
	Op  Op     `json:"op"`
	Ref string `json:"ref"`

	// Kind says which namespace ID belongs to. Absent on an entry written
	// before there were two, which means a ticket.
	Kind Kind `json:"kind,omitempty"`

	// Lease is the ref SHA the remote last confirmed to us, and it is the
	// compare-and-swap value. It survives being superseded: a second local
	// write while the first is still unsent replaces the content but keeps
	// this, because the question the remote answers is "has anyone written
	// since I last saw it", and our own unsent write is not an answer to that.
	Lease string `json:"lease"`

	// Content is the whole ticket file, not a patch. The file is the API, and
	// a queued write that replayed a diff could apply cleanly onto a state
	// nobody ever reviewed.
	Content string `json:"content,omitempty"`

	// File is the file name a milestone is committed under on its ref. Empty
	// on a ticket, and on a milestone entry an older build queued, whose ref
	// carried <key>.md.
	File string `json:"file,omitempty"`

	// Migration is set on a milestone's first write when it moves an older
	// milestone file to its ID: what that file migrated to, and where it came
	// from. It is sent as a commit of its own before Content, whatever edits
	// replaced Content in the meantime.
	Migration *Migration `json:"migration,omitempty"`

	QueuedAt  time.Time `json:"queued-at"`
	UpdatedAt time.Time `json:"updated-at"`
	By        string    `json:"by,omitempty"`
}

Entry is one ticket's pending write.

func (Entry) Age

func (e Entry) Age() time.Duration

Age is how long this write has been waiting to be sent.

type Kind added in v0.3.0

type Kind string

Kind is which namespace a queued write belongs to: a ticket, keyed by its ulid, or a milestone, keyed by its name.

It is part of the path an entry is filed under, not only a field inside it. One flat directory keyed by the bare name would collide the moment somebody named a milestone after a ticket handle — and the collision would be silent, one unsent write overwriting the other.

const (
	// KindTicket is a ticket's own ref. It is the zero value's meaning too, so
	// an entry queued by an older build — which had no kinds and no
	// subdirectory — still reads back as the ticket write it is.
	KindTicket Kind = "tickets"
	// KindMilestone is a milestone file's ref.
	KindMilestone Kind = "milestones"
	// KindEpic is an epic's ref. An epic has no file on disk, so this
	// queue is also where an unsent epic is read back from.
	KindEpic Kind = "epics"
)

type Migration added in v0.5.0

type Migration struct {
	File    string `json:"file"`
	Content string `json:"content"`
	// From is the tip of the old name ref the file came from, "none" without one.
	From string `json:"from"`
}

Migration is the move of an older milestone file to its ID, as the commit that records it holds it.

type Op

type Op string

Op is what the queued write does to the ticket's ref.

const (
	// OpWrite puts the ticket's current bytes on its ref.
	OpWrite Op = "write"
	// OpDelete removes the ref, for a ticket that has been logged or archived.
	OpDelete Op = "delete"
)

type Outcome

type Outcome string

Outcome is what became of one queued write during a flush.

const (
	// Sent means the remote accepted it and the entry is gone.
	Sent Outcome = "sent"
	// Rejected means someone else wrote the ticket first. The entry is
	// dropped: replaying it would only lose again, and the local file is still
	// there for the board to merge the ref into.
	Rejected Outcome = "rejected"
	// Unsent means the remote could not be reached. The entry stays.
	Unsent Outcome = "unsent"
	// Failed means git refused for some other reason. The entry stays, because
	// the cause is unknown and discarding a write on an unknown error is the
	// one outcome that loses work.
	Failed Outcome = "failed"
)

type Result

type Result struct {
	ID      string
	Kind    Kind
	Op      Op
	Outcome Outcome
	Err     error
	// File is the file a milestone entry was to be committed under, which is
	// what carries the name a person knows it by; empty for a ticket.
	File string
}

Result reports one entry's fate.

type Sender

type Sender interface {
	Write(id string, content []byte, lease string) (string, error)
	Delete(id, lease string) error
	WriteMilestone(key, file string, content []byte, lease string) (string, error)
	WriteEpic(name string, content []byte, lease string) (string, error)
}

Sender is the transport a flush pushes through. It is an interface so the queue's behaviour — what it keeps, what it drops, when it stops — can be tested without a remote, and so nothing in here depends on a network being reachable.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL