const MaxOpenLimit
MaxOpenLimit is the largest maxOpen a Book may be constructed with. It bounds the worst-case cost of Due and IterateOpen, which scan the open set.
Package duebook is a pure scheduling primitive for realms that need to authorize an action now and perform it later. ...
Package duebook is a pure scheduling primitive for realms that need to authorize an action now and perform it later. It is to *time* what feeledger is to *money*: it holds no coins, performs no effects, imports no chain APIs, and owns no package-level state. The importing realm owns a *Book, supplies the clock, and performs its own effects.
A realm does not ask duebook to execute anything — Gno has no autonomous execution, so "scheduled" always means "someone sends a transaction later". What a realm actually needs is for that later transaction to be authorized exactly once. Claim is that step:
1d, err := book.Claim(id, now)
2if err != nil {
3 panic(err) // not due, expired, cancelled, or already claimed
4}
5// ... the realm performs its own effect here, under its own authority
Claim checks due / not-expired / still-open and CONSUMES the deferral in the same call, before returning. The realm then acts. Replay is not guarded against, it is structurally impossible:
so at most one Claim per ID can ever succeed, across all transactions, forever. A second Claim of the same ID returns ErrNotFound whether the first one happened in this transaction or a year ago.
Because the realm performs the effect itself, no closure, callback, or capability ever crosses a realm boundary. duebook cannot be handed code to run, so it cannot be tricked into running the wrong code.
A deferral is open from Schedule until exactly one of Claim, Cancel, or Expire consumes it. There is no other transition and no way back.
1Schedule ──> open ──┬── Claim (now >= DueAt, before ExpiresAt)
2 ├── Cancel (owner only, any time while open)
3 └── Expire (anyone, once now >= ExpiresAt)
Claimability is the half-open interval [DueAt, ExpiresAt): due at DueAt, no longer claimable at ExpiresAt. A deferral scheduled with ttl == 0 never expires and has ExpiresAt == 0.
Consumed deferrals are removed, not archived — the ID counter, not a tombstone, is what prevents replay, so there is nothing to keep. Open deferrals are capped per Book at construction. Storage is therefore bounded by maxOpen regardless of how many deferrals have ever existed. Audit history belongs in the consuming realm's events.
All failures are returned as errors and leave the Book COMPLETELY UNCHANGED. Must* wrappers are the only functions here that panic.
The Book is address-agnostic: owners are non-empty strings. Realms normally use address.String().
MaxOpenLimit is the largest maxOpen a Book may be constructed with. It bounds the worst-case cost of Due and IterateOpen, which scan the open set.
MaxPayloadLen bounds a single deferral's payload. The payload is opaque to duebook — it exists so a realm can recover what it scheduled without keeping a parallel table.
1var (
2 ErrEmptyOwner = errors.New("duebook: empty owner key")
3 ErrPayloadTooBig = errors.New("duebook: payload exceeds MaxPayloadLen")
4 ErrInvalidNow = errors.New("duebook: now must be non-negative")
5 ErrInvalidDelay = errors.New("duebook: delay outside [minDelay, maxDelay]")
6 ErrInvalidTTL = errors.New("duebook: ttl must be non-negative")
7 ErrInvalidConfig = errors.New("duebook: invalid book configuration")
8 ErrBookFull = errors.New("duebook: open deferral cap reached")
9 ErrNotFound = errors.New("duebook: no such open deferral")
10 ErrNotDue = errors.New("duebook: not due yet")
11 ErrExpired = errors.New("duebook: deferral has expired")
12 ErrNotExpired = errors.New("duebook: deferral has not expired")
13 ErrNotOwner = errors.New("duebook: caller does not own this deferral")
14 ErrOverflow = errors.New("duebook: int64 overflow")
15 ErrIDExhausted = errors.New("duebook: identifier space exhausted")
16)Errors returned by Book operations.
MustNew is New but panics on error.
New returns an empty Book.
minDelay is the floor on how far ahead a deferral may be scheduled; 0 permits same-instant scheduling. maxDelay is the ceiling, and doubles as the overflow guard on now+delay. maxOpen caps simultaneously open deferrals and must be in [1, MaxOpenLimit].
Units are the caller's choice — block heights or seconds — but must be used consistently for the life of the Book.
Book holds the open deferrals of one consuming realm. The zero value is not usable; construct with New.
Cancel consumes an open deferral without performing it. Only its owner may cancel, and cancellation is permitted at any time while the deferral is open — including after it became due but before anyone claimed it.
Fails with ErrEmptyOwner, ErrNotFound, or ErrNotOwner. On error nothing is modified.
Claim consumes the deferral and returns it, authorizing the caller to perform the deferred action NOW, in this transaction.
It succeeds only while the deferral is open and now is in [DueAt, ExpiresAt). The deferral is removed BEFORE Claim returns, so a re-entrant or later Claim of the same ID finds nothing; combined with non-reused IDs, at most one Claim per ID ever succeeds.
Fails with ErrInvalidNow, ErrNotFound (never existed, or already consumed by Claim/Cancel/Expire), ErrNotDue, or ErrExpired. On error nothing is modified.
duebook does NOT check who is claiming: whether a deferral is permissionlessly claimable or restricted to its owner is the consuming realm's policy, applied before calling Claim.
Due returns up to limit open deferrals that are claimable at now, in ascending ID order (oldest first). A limit <= 0 returns nothing.
It scans the open set, so its cost is bounded by MaxOpen.
Expirable returns up to limit open deferrals that Expire would accept at now, in ascending ID order. A limit <= 0 returns nothing.
Expire consumes a deferral that is past its expiry, reclaiming its storage. It is deliberately permissionless: an expired deferral can never be claimed again, so letting anyone clear it keeps a Book from silting up with dead entries that block Schedule against maxOpen.
Fails with ErrInvalidNow, ErrNotFound, or ErrNotExpired (including for deferrals with no expiry, which never expire). On error nothing is modified.
Get returns an open deferral by ID. The second result is false if the deferral never existed or has already been consumed — Get cannot tell those apart, by design: consumed deferrals leave no tombstone.
Get is a read-only preview and never authorizes anything. Only Claim's return value authorizes an action.
IterateOpen calls fn for every open deferral in ascending ID order. Iteration stops early when fn returns true.
fn receives a COPY: Deferral is passed by value and holds only scalars and strings, so fn gets no pointer into the Book and cannot reach past it — Book's fields are all unexported.
Two rules for fn, and the second is the one that is easy to miss.
Do not Schedule, Claim, Cancel or Expire from inside fn: mutating the tree while iterating it is undefined. Collect IDs first, then act after IterateOpen returns.
fn runs under the CALLING REALM'S STORAGE AUTHORITY. This method's receiver is stamped with the importing realm's PkgID, so the borrow rules leave the realm context set to that realm for the whole callback, and a top-level fn has no receiver and no declaring realm to anchor it elsewhere. A callback that re-enters the calling realm's own mutators therefore does so with that realm's authority. Never pass a caller-supplied function here from inside a permission-gated path; pass only a closure this package's consumer wrote itself.
MaxDelay returns the Book's scheduling ceiling.
MaxOpen returns the Book's cap on simultaneously open deferrals.
MinDelay returns the Book's scheduling floor.
MustCancel is Cancel but panics on error.
MustClaim is Claim but panics on error.
MustExpire is Expire but panics on error.
MustSchedule is Schedule but panics on error.
NextID returns the identifier the next Schedule will allocate. It only ever increases, which is what makes a consumed ID unreusable.
OpenCount returns how many deferrals are currently open.
Schedule opens a deferral owned by owner, due at now+delay, expiring ttl after that (ttl == 0 means it never expires). It returns the new deferral's ID.
Fails with ErrEmptyOwner, ErrPayloadTooBig, ErrInvalidNow (now < 0), ErrInvalidDelay (delay outside [MinDelay, MaxDelay]), ErrInvalidTTL (ttl < 0), ErrBookFull, ErrOverflow, or ErrIDExhausted. On error nothing is modified.
Deferral is a scheduled action. It is returned BY VALUE: holders cannot reach into a Book through it. Payload is opaque to this package.
ExpiresAt == 0 means the deferral never expires. Otherwise the deferral is claimable exactly on [DueAt, ExpiresAt).
IsClaimable reports whether Claim would succeed at now, assuming the deferral is still open.
IsDue reports whether the deferral has reached its due time at now.
IsExpired reports whether the deferral is past its expiry at now. A deferral with no expiry is never expired.