Documentation
¶
Overview ¶
Package bundles wires the HTTP bundles surface onto the SDK's per-space bundles registry (any-sync-sdk docs/bundles.md).
A bundle is one root object registered under a stable id, with every setup object hanging off it, so the converged root id transitively names the whole install and children are re-derivable on any device.
The root comes in two shapes. A CREATED root gets a fresh id, so two devices installing while apart mint two: the registry converges on one winner, keeps the others in Bundle.Losers, and children bind by ParentId so cleaning up a loser is one cascade delete. A DERIVED root (Install.Derived) is computed from the bundle id, so every device lands on the same one and no fork is possible — at the price of permanence, a derived tree being undeletable.
The server registers nothing of its own: clients declare what they install, so this package is a generic engine, not a catalog. Deleting a loser is the CLIENT's decision — only it knows whether the loser's content was worth merging — so the engine only enforces what is decidable without knowing the content: a created install waits for the registry to converge before minting a root, and a loser is deletable only once it has demonstrably stopped arriving.
Index ¶
- Constants
- Variables
- func Child(ctx context.Context, sp space.Space, b space.Bundle, seed, typeId string, ...) (string, error)
- func ReservedId(id string) bool
- type Install
- type Resolver
- func (r *Resolver) Ensure(ctx, createCtx context.Context, sp space.Space, inst Install) (space.Bundle, bool, error)
- func (r *Resolver) Get(ctx context.Context, sp space.Space, bundleId string) (space.Bundle, error)
- func (r *Resolver) List(ctx context.Context, sp space.Space) ([]space.Bundle, error)
- func (r *Resolver) Reset()
- func (r *Resolver) Resolve(ctx context.Context, sp space.Space, bundleId, loserRootId string) error
- func (r *Resolver) ResolveRetry(ctx context.Context, sp space.Space, bundleId, loserRootId string)
- func (r *Resolver) Setup(ctx, createCtx context.Context, sp space.Space, installs []Install, ...) ([]SetupResult, error)
- func (r *Resolver) WaitConverged(ctx context.Context, sp space.Space) bool
- type SetupError
- type SetupResult
Constants ¶
const ( // DefaultGrace is the quiescence delay before a losing root may be // deleted. DefaultGrace = 5 * time.Minute // DefaultIndexWait bounds the registry-convergence wait on the // install path. Short: it rides a client request, and refusing is // correct — the client retries. DefaultIndexWait = 30 * time.Second // DefaultOfflineIndexWait is the same wait with nobody to hear // from: long enough for a peer that is mid-dial to land, short // enough that an offline device is not stalled for nothing. DefaultOfflineIndexWait = 3 * time.Second )
const ReservedIdPrefix = "system:"
ReservedIdPrefix marks the bundle ids the server's embedded catalog owns. A client install under it is refused; the prefix is the rule, the catalog its only writer.
Variables ¶
var ( // ErrNotInstalled reports that the space carries no live record for // the bundle id. ErrNotInstalled = errors.New("bundle not installed") // ErrRootNotLocal reports that the winning root's tree has not // reached this device, so its id is not writable yet. Retryable — // the answer changes as the space syncs. ErrRootNotLocal = errors.New("bundle root not local") // ErrRegistryNotSynced reports that a member could not converge the // space's registry before installing, so the install would be made // blind. Retryable. ErrRegistryNotSynced = errors.New("bundle registry not synced") // ErrLoserNotReady reports that a losing root is still arriving: // not yet fully synced, or still inside the quiescence window. // Retryable. ErrLoserNotReady = errors.New("bundle loser not ready") )
Functions ¶
func Child ¶
func Child(ctx context.Context, sp space.Space, b space.Bundle, seed, typeId string, collections ...string) (string, error)
Child derives one setup object under a bundle root. Deterministic per (space, root, seed): every device derives the same id from the winner, opens it immediately and lets the content sync in. Deleting the root cascade-deletes it.
A DERIVED root cannot be a parent — any-sync rejects a derived object as a ParentId — so a derived install's children hang off it by seed instead, with the root id folded in. They converge just as well (the root id is canonical), and the cascade the parent binding buys is moot on a root that can never be deleted.
Seeds are permanent — bump the version suffix for a successor object rather than reusing one.
func ReservedId ¶
ReservedId reports whether a bundle id is under the server's prefix.
Types ¶
type Install ¶
type Install struct {
// Id is the registry record id — permanent and versioned.
Id string
// Name is the display name. The SDK stamps it as `any.name` on a
// freshly created root, which is also what puts the root's tree in
// the head-sync diff.
Name string
// RootType is the root's one type (`any.type`) — refused next to a
// declaration, whose root carries its marker there. RootCollections
// are the collections the root is filed under at birth (a `miniapp`
// root); on a derived or declaring root a collection the root lacks
// is added on adopt.
RootType string
RootCollections []string
// RootProperties seeds the root's property values, keyed
// owner → propId → value.
RootProperties map[string]map[string]any
// Derived installs the bundle on the root DERIVED from its id
// instead of a created one: the same root id on every device,
// computed offline, so concurrent installs cannot fork and the
// convergence gate below has nothing to protect.
//
// The price is permanence — a derived tree cannot be deleted, so
// the bundle can never be uninstalled. For setups that must exist
// on both sides of a partition (a space's chat, and above all a
// 1-1's, where nobody is the owner) that is the point; for
// anything a user may remove it is the wrong trade.
Derived bool
// XKey is the root definition's handle (`type.xkey` /
// `collection.xkey`) — what a client resolves it by and what other
// declarations' relation targets name. An XKey alone declares a
// marker (no columns, no parts). Written on install; adopt never
// patches it.
XKey string
// Parts are declared on the root at install (derived or created);
// the root is then a type definition (typeId = rootId) — objects of
// that type take its datasets, and so does the root itself (a
// definition implements itself). Refused with Collection.
Parts []space.PartDraft
// Properties are declared on the root at install with ids derived
// from (root, xKey), so concurrent installs mint one column per
// handle. Every draft carries an XKey.
Properties []space.PropertyDraft
// Layout and Hidden seed the root definition's metadata on
// install; adopt never patches them. Hidden is explicit. They need
// a declaration — the SDK refuses them alone; Layout is refused
// with Collection.
Layout map[string]any
Hidden bool
// Collection makes the declaration a collection instead of a type.
Collection bool
// SystemInstall marks the server's own catalog install: it lifts the
// reserved-module refusal (the SDK's SystemInstall ensure option).
// Never set from client input.
SystemInstall bool
}
Install is one bundle to register, as the caller declared it.
func (Install) DeclaresCollection ¶
func (Install) DeclaresType ¶
DeclaresType reports whether the install makes the root a type definition — Parts, Properties or an XKey without Collection (the SDK's EnsureBundleRequest.DeclaresType rule); DeclaresCollection the collection form; Declares either.
type Resolver ¶
type Resolver struct {
// Grace is how long a losing root must have been observed before
// it may be deleted. Set at construction; tests shorten it.
Grace time.Duration
// RetryDelay is the first backoff step of ResolveRetry; each pass
// doubles it up to a ten-minute cap. Set at construction; tests
// shorten it.
RetryDelay time.Duration
// IndexWait bounds the convergence wait an install runs before
// minting a root — for a derived install too, where expiring it
// is not a refusal but is not free either (see converge). Set at
// construction; tests shorten it.
IndexWait time.Duration
// OfflineIndexWait replaces IndexWait when no peer is connected.
// The wait buys information only from peers we can reach; with
// none, thirty seconds of retrying learns exactly what the first
// second did. Set at construction; tests shorten it.
OfflineIndexWait time.Duration
// Quiescent reports whether a root has stopped receiving changes,
// so what is projected locally is the whole of it. Set at
// construction to the SDK's per-object sync state; tests
// substitute it, since an offline device never settles.
Quiescent func(sp space.Space, objectId string) bool
// contains filtered or unexported fields
}
Resolver installs bundles and deletes their losing roots, carrying the timing decisions that must not be re-made from scratch on every call.
A losing root is deleted only once it has stopped moving: its tree arrives change by change, so a client that merged "everything" from a half-arrived loser merged only what had landed. Resolver therefore requires the SDK to report the root as fully synced AND waits out Grace from the first time this process saw the loser listed.
func NewResolver ¶
NewResolver builds a Resolver with the given quiescence delay.
func (*Resolver) Ensure ¶
func (r *Resolver) Ensure(ctx, createCtx context.Context, sp space.Space, inst Install) (space.Bundle, bool, error)
Ensure adopts the space's existing install or creates one, reporting which happened.
Adoption is a pure read — no registry write, so a reader or guest member can resolve an install they may not create.
Installing waits for the registry to converge first. It rides the space's index tree, and a member that ensures against state it has not synced yet reads "nothing installed" and mints a root competing with the one already out there. WaitIndexSynced is the gate rather than a bare head-sync round: a nil round is not proof of convergence (any-sync swallows per-peer failures), while the wait also demands the Synced rollup — and its local fast path keeps an offline owner of an already-seeded space instant.
When the wait cannot complete, who is asking decides: the space's OWNER installs anyway — nobody else could have installed into a space only this account has, and its own devices converge through the registry — while any other member is refused with ErrRegistryNotSynced rather than left to fork. A DERIVED install is never refused: its root id is a pure function of the bundle id, so there is no competing root to mint.
createCtx runs the create-and-register section and should outlive the caller's request: a cancellation between minting the root and registering it leaves an orphan object nothing references.
The winner is provisional until the space syncs. ErrRootNotLocal means a winner exists but its tree has not arrived, so there is no id worth handing back yet — except for a derived winner, which this device mints for itself instead of refusing.
func (*Resolver) Get ¶
Get returns the space's registry row for the bundle id. ErrNotInstalled when there is none locally.
func (*Resolver) Reset ¶
func (r *Resolver) Reset()
Reset forgets every loser observation and in-flight retry claim: the account behind the spaces is going away, so nothing recorded here applies to what boots next. Callers join the retry goroutines first; a straggler's release only deletes an absent key.
func (*Resolver) Resolve ¶
Resolve deletes one losing root, cascading to its derived children. The caller has already merged whatever mattered out of it — the server never merges, because only the client knows what its content means.
Verdicts that cannot change come first: a target that is the current winner or was never claimed is ErrBundleNotLoser, and a claimed root no longer listed as a loser is already gone, so the call is idempotent. What remains is timing — ErrLoserNotReady while the loser is still arriving, since a client cannot have merged content that has not landed.
func (*Resolver) ResolveRetry ¶
ResolveRetry keeps trying one loser with backoff, for the window a client has already asked for: the merge decision was made, only the timing is missing. Blocking — callers own the goroutine, and must pass a context that outlives the request that triggered it. At most one loop per loser; dropped on restart, and the client's retry re-arms it.
func (*Resolver) Setup ¶
func (r *Resolver) Setup(ctx, createCtx context.Context, sp space.Space, installs []Install, beforeInstall func(ctx context.Context, sp space.Space, inst Install) error) ([]SetupResult, error)
Setup ensures an ordered list of installs — a usecase and its dependencies — against ONE registry-convergence wait. Ensure waits before every install it cannot adopt, so a non-owner with an unconverged registry would otherwise pay the full wait per bundle before its refusal; here the first install that needs the verdict pays it, the rest reuse it.
beforeInstall runs before a root is minted for an entry (never on adopt, never on a declaration heal): the caller's own pre-install rules, such as a handle-conflict check. A non-nil error stops the walk at that entry.
func (*Resolver) WaitConverged ¶
WaitConverged runs the registry-convergence wait the install gate uses, bounded by the same knobs (offline fast expiry), and reports whether the registry converged. The read-side lock for the bundles surface: a read that answers after true reports definitive absence; after false the caller labels the read provisional.
type SetupError ¶
SetupError names the install a Setup call failed on. The results before it stand — every step is idempotent, so the caller re-runs the whole setup and resumes.
func (*SetupError) Error ¶
func (e *SetupError) Error() string
func (*SetupError) Unwrap ¶
func (e *SetupError) Unwrap() error