Documentation
¶
Overview ¶
Package subscribe owns the per-space live-query engine and the projected Event types the apply path hands to it. One Engine per loaded space; afterApply builds an Event via BuildEvent and calls Engine.OnApply, which routes each event to matching querySubs whose windows are maintained incrementally.
Two subscription scopes:
- Shared-objects: fires on every event whose dataset is the per-space `objects` collection. Reached via Space.QueryObjects().
- Per-(objectId, dataset): fires only on exact matches.
Per-sub window: entries map[string]*entry (id -> sort tuple) plus minRef/maxRef pointers for O(1) boundary access. When Limit > 0 the engine holds limit+1 entries so the largest-tuple entry serves as a sentinel; single-event shifts (top-of-sort arrivals) absorb cleanly without an any-store query. When too many records leave the held window without replacement (>= DriftBudgetPercent of limit), the sub closes with space.ErrSubscriptionDrifted; mailbox overflow closes with space.ErrSubscriptionOverflow. Both signal "resubscribe".
Locking: a single engine.mu serializes register / close / OnApply. No per-sub locks. Holding engine.mu during the initial snapshot fences the apply path so the new sub never misses an event.
Index ¶
- Constants
- Variables
- type Engine
- func (e *Engine) Close() error
- func (e *Engine) HasSubscribers() bool
- func (e *Engine) NotifyDeleted(spaceId, dataset, objectId string)
- func (e *Engine) OnApply(ev Event, postValue PostValueFn)
- func (e *Engine) SpaceId() string
- func (e *Engine) Subscribe(cfg SubConfig, snapshot SnapshotFn) (*Sub, error)
- type Event
- type EventRecord
- type PostValueFn
- type Scope
- type SnapshotFn
- type Sub
- type SubConfig
Constants ¶
const ObjectsDataset = properties.Dataset
ObjectsDataset is the CRDT dataset whose changes feed the shared-objects scope — the per-space `objects` collection, where every object's property values land regardless of object kind.
User-facing terminology calls these "property values"; the dataset is named "objects" because each row is one object's values record. Unrelated to typetype.DatasetPropertyDefs (type-definition metadata on type objects).
Variables ¶
var ErrEngineClosed = errors.New("subscribe: engine closed")
ErrEngineClosed is returned by Subscribe after Close.
var ErrLimitWithoutSort = errors.New("subscribe: Limit > 0 requires Sort")
ErrLimitWithoutSort fires when a caller asks for Limit > 0 without supplying a Sort — the engine needs a total order to define "boundary" / "sentinel". Limit == 0 (unbounded) is fine without Sort.
Functions ¶
This section is empty.
Types ¶
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the per-space live-query engine. Cheap when no subs are registered: HasSubscribers is a single atomic load and the afterApply hook short-circuits on it.
func (*Engine) HasSubscribers ¶
HasSubscribers reports whether any sub is live. Single atomic load — call before paying the cost of building the wire Event in afterApply.
func (*Engine) NotifyDeleted ¶
NotifyDeleted emits a synthetic delete event for objectId in the given dataset scope, so live subscriptions drop the row and report RemoveDeleted — without an underlying CRDT apply.
The delete-callback path (spaceobjects.Store.DeleteTree, fired when any-sync catches a settings-tree deletion) uses this: it removes the materialized row directly, device-local, instead of writing a CRDT tombstone into the tree that is being reclaimed. The post-value lookup is a constant nil — a deleted record carries no post-apply doc, and applyRecord skips the lookup for Deleted records anyway.
The emitted SubscriptionEvent carries an empty VersionId (there is no underlying DAG change to draw an orderId from). A Removed{RemoveDeleted} therefore cannot be version-fenced — consumers must not drop a Removed via the VersionId replay fence; act on it directly.
func (*Engine) OnApply ¶
func (e *Engine) OnApply(ev Event, postValue PostValueFn)
OnApply is the apply-path hook. afterApply calls this with the same Event built once for the dispatcher, plus the PostValueFn that lets the engine evaluate filter / sort against the post-apply doc.
Cheap when there are no subs (HasSubscribers check returns false before this is even invoked by the caller). When subs exist, takes engine.mu briefly to fan to the matching ones.
func (*Engine) Subscribe ¶
func (e *Engine) Subscribe(cfg SubConfig, snapshot SnapshotFn) (*Sub, error)
Subscribe registers a new querySub. The snapshot callback runs under engine.mu so the apply path is fenced between the snapshot read and the sub's registration — no events are missed and no events fire against an unregistered sub.
Returns space.ErrSubscribeUnsupported when the engine is closed. Returns ErrLimitWithoutSort when cfg.Limit > 0 and cfg.Sort is nil.
type Event ¶
type Event struct {
SpaceId string
ObjectId string
Dataset string
VersionId crdt.VersionId
Records []EventRecord
}
Event is a single CRDT apply event handed to the engine.
Pipeline contract: receive change → apply to CRDT → commit the any-store tx → THEN build this event. By the time a consumer sees an Event, every projected $set/$unset is already durable in the controller's any-store; a Query against the same dataset run from the same process reflects the same state.
Carries the routing tuple plus enough about the change for the engine to classify each record (filter + sort + window membership) without re-querying:
VersionId — per-change DAG order. Forwarded on the wire as SubscriptionEvent.VersionId so fence-and-replay consumers can dedupe across snapshots.
Records — the post-apply effect of the change, projected to a flat list of $set / $unset ops per record. Ops payloads are deep-cloned off the controller's storage onto event-owned arenas, so an Event is safe to retain past the build call.
func BuildEvent ¶
func BuildEvent(ch *crdt.Change, recordIds []string, derivedOps [][]crdt.Op, postValue PostValueFn) Event
BuildEvent projects (ch, recordIds, derivedOps, postValue) into the wire Event shape — runs the same per-record projection an engine caller would need, with deep-cloned op payloads. afterApplyFor builds this once per change before handing it to Engine.OnApply.
Safe to call with a nil change — returns a zero Event in that case; callers should check ch != nil themselves if they want to skip work.
type EventRecord ¶
type EventRecord struct {
Id string
// Created is true when this change first materialised the record.
// The engine uses it (combined with sentinel/visibility state) to
// decide Added vs Updated emit semantics.
Created bool
// Deleted is true when the change tombstoned this record. Ops is
// empty in that case; the engine drops the entry from its held
// set.
Deleted bool
// Ops is the projected $set / $unset operations on the post-apply
// record. Empty when Deleted is true. Surfaced verbatim to
// SubscriptionEvent SubRecord.Ops for atomic-update consumers.
Ops []space.EventOp
}
EventRecord is one record's worth of projected change inside an Event. Id is the record id within Dataset (for shared per-space datasets like "objects" this equals ObjectId).
type PostValueFn ¶
PostValueFn returns the post-apply value for one record in the change being built. Provided by the per-space layer (the spaceobjects Store holds the controller). Index is into ch.Records.
May return nil for tombstoned / dropped records (e.g. a gated op that didn't land); callers should treat nil as "the record no longer exists at this point in the timeline".
type Scope ¶
Scope locates which events a sub cares about.
Shared=true means "every event whose dataset == ObjectsDataset across every object in the space" — the property firehose. The ObjectId/Dataset fields are ignored in this case.
Shared=false means "events whose ObjectId/Dataset match exactly".
type SnapshotFn ¶
SnapshotFn is the caller-supplied populator. It runs UNDER engine.mu (held by Subscribe), so apply events fired during the read block waiting on engine.mu and process after the new sub is registered. The callback yields each result row's (id, post-doc) to the engine, which extracts the sort tuple via cfg.Sort and inserts the entry.
The callback must only read already-materialised rows. Anything that can apply a change — loading an object (its replay runs OnApply), writing to the DAG — needs engine.mu itself and deadlocks the engine. Resolve objects before calling Subscribe.
type Sub ¶
type Sub struct {
// contains filtered or unexported fields
}
Sub is the consumer-side handle returned by Engine.Subscribe. It implements space.QuerySubscription. The engine guarantees the mailbox carries only post-snapshot transitions (no replay of the snapshot itself).
type SubConfig ¶
type SubConfig struct {
Scope Scope
Filter query.Filter // already ANDed with `_deletedAt missing`
Sort query.Sort // required when Limit > 0
Limit int // 0 = unbounded; no sentinel, no fast-reject
MailboxCap int // 0 → defaultMailboxCap; clamped to >= minMailboxCap
DriftBudget int // percent of limit; 0 → defaultDriftBudgetPercent; ignored when Limit == 0
}
SubConfig is the input to Engine.Subscribe. Built by queryImpl from the chained Filter / Sort / Limit + caller's QueryOpts. The engine owns the parsed query.Filter / query.Sort after registration; the caller must not mutate them.