store

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package store is a workspace on local disk: a roster, one append-only inbox per node, a global seq counter, and one cursor per node.

Layout under the workspace root:

lock             flock target, held while assigning a seq
seq              global counter, decimal
roster           "<node> <joined ts>" per line
inbox/<node>.log current segment, append-only
inbox/<node>.log.<n> rotated segments, higher n is newer
cursor/<node>    last seq this node has read

Index

Constants

This section is empty.

Variables

View Source
var ErrHangup = errors.New("hangup")

ErrHangup reports that the caller's cancel channel closed. In serve mode that means the client went away.

View Source
var ErrTakenOver = errors.New("cursor taken over")

ErrTakenOver reports that a watcher was asked to hand its cursor back.

View Source
var ErrWatched = errors.New("already being watched")

ErrWatched reports that another process already reads this cursor.

View Source
var MaxSegment int64 = 1 << 20

MaxSegment is the size at which a log rotates. Seq continues across segments; it is a counter, not a line number. Tests shrink it so they can cross it without writing a megabyte through fsync.

View Source
var Warnf = func(format string, args ...any) {
	fmt.Fprintf(os.Stderr, "beb: "+format+"\n", args...)
}

Warnf reports a non-fatal storage anomaly -- corruption a reader stopped at, which would otherwise be silent. It defaults to stderr and is a var so a test can capture it. Storage stays local, single-writer, and fsync'd, so this should never fire; when it does, it is a real disk or out-of-band-edit problem an operator needs to see.

Functions

This section is empty.

Types

type Member

type Member struct {
	Node   string
	Joined time.Time
}

Member is one roster entry.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is an open workspace.

func Open

func Open(dir string) (*Store, error)

Open prepares a workspace rooted at dir, creating it if absent.

func (*Store) CreateTopic added in v0.1.3

func (s *Store) CreateTopic(name, creator string) error

CreateTopic registers a topic if it is new, and is idempotent: a topic that already exists keeps its original creator and time. Unlike the roster, any member may create a topic -- a topic grants no identity and no access, it only names a shared inbox -- which is why this is reachable over the wire where node membership is not.

func (*Store) Cursor

func (s *Store) Cursor(node string) (uint64, error)

Cursor is the last seq this node has read. Zero means it has read nothing, so mail that arrived while it was away is still waiting.

func (*Store) InitTopicCursor added in v0.1.3

func (s *Store) InitTopicCursor(node, topic string, seq uint64) error

InitTopicCursor places a node's cursor into a topic, writing the cursor file even at 0. That file is the subscription marker: without it a later read would re-place the cursor and miss mail that arrived in between. A remote client uses this to record a fresh subscription at the tail the relay reported, since the topic log itself lives on the relay. Written directly rather than through SetTopicCursor, which is monotonic.

func (*Store) IsTopic added in v0.1.3

func (s *Store) IsTopic(name string) (bool, error)

IsTopic reports whether a topic is registered. Sending to an unregistered topic is refused the same way sending to a non-member is: a post into a topic nobody created is mail nobody will ever read.

func (*Store) Join

func (s *Store) Join(node string) error

Join adds a node to the roster. Rejoining is a no-op: it must not reset the cursor, or mail that arrived while the node was away would be re-read or skipped.

func (*Store) Leave

func (s *Store) Leave(node string) error

Leave removes a node from the roster and drops its cursor. The inbox log is left on disk: it is a record, and deleting mail is not this verb's job.

func (*Store) LockWatch added in v0.2.1

func (s *Store) LockWatch(name string) (*WatchLock, error)

LockWatch claims the sole right to read a cursor, for as long as the returned closer lives.

A cursor has exactly one owner. Two watchers of one inbox each drain what the other should have seen, and neither can tell: the mail is gone, the cursor moved, nothing errors. That failure has no signature at all, so it must be made impossible rather than diagnosable -- an advisory flock the kernel drops on exit, so a crashed watcher leaves no stale claim to clean up.

The name is the cursor's: a node, or "<node>:#topic" for a topic, since those are separate cursors with separate owners.

func (*Store) PostToTopic added in v0.1.3

func (s *Store) PostToTopic(from, topic, body string, now time.Time) (uint64, error)

PostToTopic appends a message to a topic and, for each workspace member named with @, drops a provenance-tagged copy into that member's personal inbox. A member need not watch a topic to be reached in it; the ping names the topic and the seq, so the reader can act on it or pull a context window without hunting for where the message lives. @all is not a member, so it reaches no one. Returns the topic seq.

func (*Store) Read

func (s *Store) Read(node string, after uint64) ([]wire.Msg, error)

Read returns the messages in a node's inbox with seq greater than after, oldest first.

func (*Store) ReadLast added in v0.6.0

func (s *Store) ReadLast(node string, n int) ([]wire.Msg, error)

ReadLast returns the final n messages of an inbox, whatever their seqs.

Seq is workspace-global, so an inbox's numbers are sparse: "the last twenty" cannot be computed by subtracting twenty from anything. It has to be counted, which is what this does -- and it is the shape people actually ask for, because nobody remembers the number they want.

func (*Store) RequestTakeover added in v0.4.0

func (s *Store) RequestTakeover(name string, wait time.Duration) (*WatchLock, error)

RequestTakeover asks whoever holds this cursor to hand it back, then waits for the lock.

Locking first-come is right until the first comer is a process nobody is listening to any more -- a session resumed by a supervisor, a loop outliving the window that started it. Mail then goes on being read, correctly, by no one. This exists so a deliverer that a human just configured can reclaim the cursor from a reader that a human has forgotten, which is a judgement only the caller can make: agents ask for a lock, infrastructure asks for a takeover.

func (*Store) Root

func (s *Store) Root() string

Root is the workspace directory.

func (*Store) Roster

func (s *Store) Roster() ([]Member, error)

Roster lists members in join order.

func (*Store) Send

func (s *Store) Send(from string, to []string, body string, now time.Time) (uint64, error)

Send appends one message to each recipient's inbox under a single seq. The message is one message, so it gets one number; it simply lands in several inboxes. Returns the assigned seq once every append is durable.

func (*Store) SetCursor

func (s *Store) SetCursor(node string, seq uint64) error

SetCursor advances the cursor. It never moves backwards.

func (*Store) SetTopicCursor added in v0.1.3

func (s *Store) SetTopicCursor(node, topic string, seq uint64) error

SetTopicCursor advances a node's cursor into a topic. Like every cursor, it never moves backwards.

func (*Store) SubscribeTopic added in v0.1.3

func (s *Store) SubscribeTopic(node, topic string) error

SubscribeTopic registers a topic and, on a node's first subscription, places its cursor at the tail so bare reads deliver only new messages. Returning subscribers keep the cursor they had. History stays available on demand by reading from an explicit seq.

func (*Store) TakeoverRequested added in v0.4.0

func (s *Store) TakeoverRequested(name string) bool

TakeoverRequested reports whether someone is asking for this cursor.

Presence, not timestamps: comparing a file's mtime against a moment held in memory looked fine on a filesystem with nanosecond stamps and failed on one that rounds to the second, where the marker appeared to predate the lock it was meant to revoke. A claim taken cleanly starts with no marker, so any marker afterwards is a request.

func (*Store) TopicCursor added in v0.1.3

func (s *Store) TopicCursor(node, topic string) (uint64, error)

TopicCursor is how far a node has read into a topic. Each reader keeps its own cursor into the one shared log.

func (*Store) TopicSubscribed added in v0.1.3

func (s *Store) TopicSubscribed(node, topic string) bool

TopicSubscribed reports whether a node has a cursor into a topic yet -- whether it has ever read or been placed into it.

func (*Store) TopicTail added in v0.1.3

func (s *Store) TopicTail(topic string) (uint64, error)

TopicTail is the seq of the last message in a topic, or 0 if empty. A fresh subscriber starts here, so a late arrival sees new traffic rather than the entire history -- context an agent would pay for and rarely want.

func (*Store) Topics added in v0.1.3

func (s *Store) Topics() ([]Topic, error)

Topics lists registered topics in creation order.

func (*Store) Unread

func (s *Store) Unread(node string) (int, error)

Unread counts what a node has not read yet.

func (*Store) UnsubscribeTopic added in v0.1.3

func (s *Store) UnsubscribeTopic(node, topic string) error

UnsubscribeTopic drops a node's cursor into a topic. The topic and its log are untouched -- this is one reader stepping away, not the channel closing.

func (*Store) Watch

func (s *Store) Watch(node string) (*Watcher, error)

Watch arms a watcher on a node's inbox.

Both the inbox directory and the log file are watched. Appends show up on the file; the directory is what survives rotation, which renames the log out from under a file watch and puts a new one in its place. The directory also covers an inbox that does not exist yet.

type Topic added in v0.1.3

type Topic struct {
	Name    string // includes the leading '#'
	Creator string
	Created time.Time
}

Topic is one entry in the topics registry. A topic is a named inbox many nodes read, each with its own cursor into the one shared log.

type WatchLock added in v0.4.0

type WatchLock struct {
	// contains filtered or unexported fields
}

WatchLock is a held cursor claim. Held is the important word: the kernel drops it when this process dies, so nothing has to be cleaned up after a crash.

func (*WatchLock) Close added in v0.4.0

func (l *WatchLock) Close() error

Close releases the claim.

func (*WatchLock) Revoked added in v0.4.0

func (l *WatchLock) Revoked() bool

Revoked reports whether someone is asking for this cursor.

func (*WatchLock) Watch added in v0.4.0

func (l *WatchLock) Watch(interval time.Duration) <-chan struct{}

Watch returns a channel closed when this claim is revoked, for loops that block rather than poll.

type Watcher

type Watcher struct {
	// contains filtered or unexported fields
}

Watcher reports that a node's mail may have changed.

Events are a hint, never the record. What a node has read is the cursor, so a duplicate event costs one wasted read and a dropped one costs only latency. That asymmetry is what lets this be an ordinary filesystem watch rather than a delivery protocol: drops are free, the cursor is sacred.

func (*Watcher) Close

func (w *Watcher) Close() error

Close releases the watch.

func (*Watcher) Wait

func (w *Watcher) Wait(backstop time.Duration, cancel <-chan struct{}) error

Wait blocks until the inbox may have changed, until backstop elapses, or until cancel is closed. A nil cancel channel simply never fires, which is what a foreground watch on a terminal wants.

The backstop is not a poll interval. Watch semantics differ across kernels, and a missed event must cost latency rather than mail, so this bounds how long a drop can go unnoticed. The common path returns on the event, long before it fires.

Jump to

Keyboard shortcuts

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