Documentation
¶
Overview ¶
Package notebook is the append-only record a stack writes and reads through txco://notebook/*: per (tenant, namespace, name) a head row that allocates sequence numbers, and entries {seq, at, type, data, object_key} in that order. Task is state; a notebook is history — "how did we get here?" for a task, a conversation, a workspace session, an import.
Storage is dialect-aware (registry.Dialect, the same seam imap, auth and the scheduled store use); the bundled backend is a SQLite file of its own. It is deliberately NOT a set of runtime tables: the dbcache watcher reloads the whole runtime mirror on any runtime-DB write, and a notebook is a high-write table by definition.
Invariants: seq is allocated from the head row's next_seq — stored, never MAX(seq)+1 — so a pruned tail never recycles a sequence number; `at` is assigned here at commit and never by the caller; every read returns entries ascending by seq; a duplicate object_key returns the original row unchanged; and a public cursor is opaque — it carries the head's generation, so a cursor from a deleted-and-recreated notebook fails loudly (StaleCursorError) instead of silently re-reading new entries as old ones.
A notebook write is not part of the transaction of the thing it describes: it is an application record, not an event-sourcing substrate.
Index ¶
- Constants
- func ErrorCode(err error) string
- func FormatAt(t time.Time) string
- func ParseAt(s string) (time.Time, error)
- func Register(name string, c Constructor)
- func ValidName(name string) error
- func ValidType(t string) error
- type AppendReq
- type AppendResult
- type CodedError
- type Config
- type Constructor
- type Cursor
- type Entry
- type Head
- type InvalidArgError
- type InvalidNameError
- type Limits
- type ListResult
- type ReadReq
- type ReadResult
- type Ref
- type StaleCursorError
- type Store
- func (s *Store) Append(ctx context.Context, req AppendReq) (AppendResult, error)
- func (s *Store) Close() error
- func (s *Store) DB() *sql.DB
- func (s *Store) Delete(ctx context.Context, tenant, namespace, name string) (bool, error)
- func (s *Store) EnsureSchema(ctx context.Context) error
- func (s *Store) ForEach(ctx context.Context, req ReadReq, fn func(Entry) error) error
- func (s *Store) Limits() Limits
- func (s *Store) List(ctx context.Context, tenant, namespace, prefix, after string, limit int) (ListResult, error)
- func (s *Store) Read(ctx context.Context, req ReadReq) (ReadResult, error)
- func (s *Store) SetLimits(l Limits)
- func (s *Store) SetTTL(ctx context.Context, ref Ref, ttl time.Duration) error
- func (s *Store) Stat(ctx context.Context, ref Ref) (Head, bool, error)
- func (s *Store) Sweep(ctx context.Context, now time.Time, batch int) (int64, error)
- type StoreError
- type TooLargeError
Constants ¶
const ( // MaxNameBytes bounds a whole name. MaxNameBytes = 250 // MaxSegmentBytes bounds one '/'-separated segment. MaxSegmentBytes = 128 // MaxTypeBytes bounds an entry type. MaxTypeBytes = 128 // MaxObjectKeyBytes bounds an object_key: it sits in a btree index, and // a Postgres index tuple caps well under this (same guard as the // scheduled store's idempotency key). MaxObjectKeyBytes = 512 )
Name grammar — the blob grammar, adopted so hierarchical names (task/42, conversation/alice, workspace/session-abc) work and a prefix listing is meaningful. KV's own grammar forbids '/' and would reject every one of those.
const AtLayout = "2006-01-02T15:04:05.000000000Z"
AtLayout is the canonical, FIXED-WIDTH UTC timestamp stored in `at`, `expires_at`, `created_at` and `updated_at`: 30 bytes always (the fraction keeps its zeros, the zone is a literal Z), so a TEXT comparison on either engine is a chronological comparison. time.RFC3339Nano trims trailing zeros and would sort "…00Z" after "…00.5Z".
Variables ¶
This section is empty.
Functions ¶
func ParseAt ¶
ParseAt reads an AtLayout timestamp, accepting any RFC 3339 precision as a fallback so caller-supplied `since`/`until` normalise through it.
func Register ¶
func Register(name string, c Constructor)
Register adds a backend constructor. Called from a backend package's init().
Types ¶
type AppendReq ¶
type AppendReq struct {
Ref Ref
Type string
Data json.RawMessage
ObjectKey string
TTL time.Duration
}
AppendReq is one append. TTL > 0 overrides the head's ttl_secs for this entry only; 0 inherits it.
type AppendResult ¶
AppendResult reports the entry's position. Existed means the object_key was already recorded: Seq and At are the ORIGINAL row's and nothing was written, so a retry is observationally identical to the first success.
type CodedError ¶
CodedError carries a stable txco_notebook_* code. The op layer surfaces it as `<into>.error.{code,message}` with a nil Go error, so an author branches with `WHEN ._notebook.error.code != ""` and the run continues — a record that silently fails is worse than no record.
type Config ¶
type Config struct {
// DBPath is the bundled SQLite backend's file path (--notebook-db-path).
DBPath string
}
Config carries backend-selecting options resolved from chassis config. The bundled "sqlite" backend uses DBPath. Adding a field here doesn't affect existing backends (same posture as imap.Config / scheduled.Config).
type Constructor ¶
Constructor builds a Store from resolved config. It is expected to open its backing DB and call Store.EnsureSchema before returning.
type Cursor ¶
Cursor is a position in a notebook. On the wire it is an opaque string (Encode); callers never build one from a seq. It carries the head's generation so a cursor outlives neither a delete nor a recreate.
func ParseCursor ¶
ParseCursor reads an Encode'd cursor; "" is the zero cursor. Anything else that does not decode — including a bare sequence number — is an InvalidArgError, so the format can evolve behind the version character.
type Entry ¶
type Entry struct {
Seq int64
At time.Time
Type string
Data json.RawMessage
ObjectKey string
ExpiresAt time.Time
}
Entry is one notebook row. Data is always a valid JSON document ("{}" when the append omitted it). ExpiresAt is zero for an unbounded entry.
type Head ¶
type Head struct {
Name string
Generation int64
HighSeq int64
TTL time.Duration
CreatedAt time.Time
UpdatedAt time.Time
}
Head is a notebook's head row. HighSeq is the last allocated sequence number (0 = never appended); TTL is the default retention for new entries (0 = unbounded).
type InvalidArgError ¶
type InvalidArgError struct{ Reason string }
InvalidArgError flags a malformed request: a bad tenant or namespace, an empty or ill-formed type, data that is not JSON or not UTF-8, a negative window, since >= until, or a cursor that does not decode.
func (*InvalidArgError) Code ¶
func (e *InvalidArgError) Code() string
func (*InvalidArgError) Error ¶
func (e *InvalidArgError) Error() string
type InvalidNameError ¶
type InvalidNameError struct{ Reason string }
InvalidNameError flags a notebook name outside the grammar (see ValidName).
func (*InvalidNameError) Code ¶
func (e *InvalidNameError) Code() string
func (*InvalidNameError) Error ¶
func (e *InvalidNameError) Error() string
type Limits ¶
type Limits struct {
// MaxDataBytes caps one entry's `data` (the KV --kv-max-value-bytes
// posture); 0 = unlimited.
MaxDataBytes int
// DefaultReadLimit is the page size when a read names none.
DefaultReadLimit int
// MaxReadLimit is the page ceiling; larger requests are clamped.
MaxReadLimit int
// MaxTTL clamps a requested TTL; 0 = unlimited.
MaxTTL time.Duration
}
Limits are the node-configured caps. Zero fields take DefaultLimits.
type ListResult ¶
ListResult is one page of heads, ascending by name. Next is the last name when more remain, "" otherwise.
type ReadReq ¶
type ReadReq struct {
Ref Ref
After Cursor
UpTo int64
Since time.Time
Until time.Time
Tail int
Type string
Limit int
}
ReadReq selects entries. Selection varies; the order never does — every result is ascending by seq.
- After: exclusive cursor (the head walk); zero = from the start.
- Since/Until: `at` in [Since, Until); zero = unbounded on that side.
- Tail: the newest N of the selection, still returned ascending.
- Type: only entries of that type.
- Limit: page cap; 0 = DefaultReadLimit, clamped to MaxReadLimit.
- UpTo: an inclusive seq ceiling ForEach uses to snapshot a walk. Not on the op surface.
type ReadResult ¶
ReadResult is one page. Next is the cursor for the following page and is non-zero only when the page was full (the LOOP … UNTIL next == "" drain idiom); in tail mode it is always zero, because rows clipped there are older than the window and not reachable by a forward cursor. Cursor is the position after the last returned entry whenever Count > 0 — what a poller feeds back as After — and equals Next when Next is non-zero.
type Ref ¶
Ref identifies a notebook: unique within (tenant, namespace), never addressable across tenants. Tenant and namespace are single segments (the KV grammar: no '/', no control characters); the name follows the blob grammar so hierarchical names like task/42 work and prefixes are enumerable.
type StaleCursorError ¶
type StaleCursorError struct{}
StaleCursorError is returned when a cursor's generation is not the notebook's current one: the notebook was deleted and recreated since the cursor was issued, so continuing would silently re-read new entries as old. Restart from the beginning.
func (*StaleCursorError) Code ¶
func (e *StaleCursorError) Code() string
func (*StaleCursorError) Error ¶
func (e *StaleCursorError) Error() string
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the façade over the two tables. It carries the dialect (for `?`→`$n` rebinding), the node limits, and a clock seam for tests, mirroring imap.Store.
func NewStore ¶
NewStore builds a Store over the opened DB and its dialect. A nil dialect defaults to SQLite (the in-tree default).
func Open ¶
Open constructs the named backend. Unknown name is a startup error listing what is available (so a misconfigured --notebook-store fails loudly).
func (*Store) Append ¶
Append records one entry and returns its position. Validation happens before any DB work; every DB failure comes back as a StoreError.
Appends to one notebook are serialized: the transaction first locks the head row (LockClause — FOR UPDATE on Postgres; on SQLite the _txlock=immediate connection already holds the write lock), so the object_key check and the next_seq allocation can never interleave across nodes. A unique-index violation (an out-of-band writer that won the same key) is retried once, in a fresh transaction — Postgres aborts the current one on a violation.
func (*Store) Delete ¶
Delete removes the notebook: its entries, then its head, in one transaction under the head lock, so a concurrent Append re-evaluates to the auto-create path with no orphan rows to collide with — and the recreated head carries a new generation, which is what invalidates every cursor issued before the delete. Returns false when there was nothing.
func (*Store) EnsureSchema ¶
EnsureSchema creates the tables + indexes if absent. Portable DDL (the imap store's rules): TEXT ids, TEXT fixed-width timestamps, JSON as TEXT, BIGINT counters, native partial indexes — one statement set serves SQLite and Postgres. There is no migration runner for these stores; changes must be additive.
func (*Store) ForEach ¶
ForEach streams the selection oldest→newest without materializing more than one page: it snapshots the head's HighSeq as the ceiling (so a hot notebook cannot make an export run forever), then walks pages of MaxReadLimit. fn's error aborts the walk and is returned as is. A tail request is a single page.
func (*Store) List ¶
func (s *Store) List(ctx context.Context, tenant, namespace, prefix, after string, limit int) (ListResult, error)
List returns one page of the (tenant, namespace) heads whose name starts with prefix, ascending by name. after is the last name of the previous page. Prefix matching is substr equality, not LIKE ('_' is legal in a name) and not a byte-range on the id (wrong under a non-C Postgres collation).
func (*Store) Read ¶
Read returns one page of the selection, ascending by seq. A notebook that does not exist reads as empty; a cursor from an earlier generation is a StaleCursorError.
func (*Store) SetLimits ¶
SetLimits installs the node's caps. A zero page size or ceiling keeps the default; MaxDataBytes and MaxTTL are taken as given (0 = unlimited).
func (*Store) SetTTL ¶
SetTTL sets the head's default retention for new entries (0 = unbounded), creating the head if needed. Existing entries keep their expiry.
func (*Store) Stat ¶
Stat returns the head row. ok is false when the notebook has never been appended to (or was deleted).
type StoreError ¶
StoreError wraps a database or driver failure.
func (*StoreError) Code ¶
func (e *StoreError) Code() string
func (*StoreError) Error ¶
func (e *StoreError) Error() string
func (*StoreError) Unwrap ¶
func (e *StoreError) Unwrap() error
type TooLargeError ¶
TooLargeError flags a field over its byte cap. Its own code so an author can branch "trim the payload and retry" apart from "malformed".
func (*TooLargeError) Code ¶
func (e *TooLargeError) Code() string
func (*TooLargeError) Error ¶
func (e *TooLargeError) Error() string