Documentation
¶
Overview ¶
Package space is the public caller surface of the SDK. Everything middleware reaches for lives here:
- Space, SpaceService — lifecycle: Create / Join / Derive / Delete
- ACL — invite, accept/decline, change perms, ownership
- Members — members system collection
- Query, Subscription — read + event flow over any-store
- Agg — MongoDB-style aggregation pipelines (snapshot-only)
- ModifyBatch — writes (CRDT ops) returning VersionId
- TypesAPI — type objects and property definitions
- PropertiesAPI — per-scope property writes (base/account/device)
- SyncStatus — per-space/object/peer status accessor
- Indexer — seam the techspace implementation plugs into
- VersionId — re-exported alias of internal/crdt.VersionId
See docs/space.md, docs/data-structure.md, and docs/crdt.md.
Index ¶
- Constants
- Variables
- func ActiveDevice(devices []Device, app string) (peerId string, ok bool)
- func CompareVersion(a, b VersionId) int
- func DeriveAccountMetadataSymKey(accountKey crypto.PrivKey) (crypto.SymKey, error)
- func EncodeAccountMetadata(m AccountMetadata) []byte
- func EncodeInvite(i Invite) (string, error)
- func EncryptProfile(meta AccountMetadata, key crypto.SymKey) ([]byte, error)
- func MarshalSymKey(k crypto.SymKey) (string, error)
- func UnmarshalSymKey(s string) (crypto.SymKey, error)
- type ACL
- type AccountMetadata
- type Agg
- type AttachOpts
- type Bundle
- type BundlesAPI
- type CRDTVersionNewerError
- type CRDTVersionState
- type ChangeIndexAPI
- type ChangeList
- type ChangeMeta
- type CoalesceOpts
- type CollectionCreateParams
- type CollectionInfo
- type CollectionPatch
- type CollectionsAPI
- type CreateObjectOpts
- type CreateRequest
- type DatasetDef
- type DatasetDefPatch
- type DatasetDiff
- type DatasetDraft
- type DatasetFieldDef
- type DatasetFieldDraft
- type DatasetSchema
- type DebugAPI
- type DeleteBatch
- type DeletePolicy
- type DeriveObjectOpts
- type DeriveRequest
- type Device
- type DeviceClaim
- type DeviceUpsert
- type DiffFilter
- type DiffKind
- type DiffResult
- type EnsureBundleRequest
- type EnsureOption
- type EnsureOptions
- type EventOp
- type FieldDiff
- type FileInfo
- type FileListOpts
- type FileReader
- type FileStats
- type FileStatus
- type FileSyncState
- type Files
- type HistoricalView
- type HistoryAPI
- type HistoryFilter
- type IdRule
- type IdentitiesAPI
- type IdentityInfo
- type IdentityListEvent
- type Indexer
- type Invite
- type InviteInfo
- type InviteKind
- type Iterator
- type JoinRequest
- type JoinRequestInfo
- type Member
- type MemberAdd
- type MemberEvent
- type MemberEventKind
- type MemberStatus
- type MembersAPI
- type ModifyBatch
- type ModifyResult
- type Mutability
- type ObjectChange
- type ObjectDebug
- type ObjectReadState
- type ObjectService
- type ObjectSyncStatus
- type Op
- type OpRejection
- type OpType
- type P2PState
- type PartDef
- type PartDraft
- type PayloadRow
- type PayloadsView
- type PeerSyncStats
- type Permission
- type PermissionChange
- type ProjectionOpts
- type PropertiesAPI
- type PropertyDef
- type PropertyDefsAPI
- type PropertyDraft
- type PropertyKind
- type PropertyPatch
- type PubSubAPI
- type PubSubMessage
- type PushAPI
- type PushKeys
- type PushPlatform
- type PushSpaceTopics
- type PushSubscription
- type Query
- type QueryOpts
- type QueryResult
- type QuerySubscription
- type ReadStateAPI
- type RecordDiff
- type RecordModify
- type RemoveReason
- type RemovedRecord
- type Scope
- type SearchFields
- type Service
- type SetMetadataRequest
- type Space
- type SpaceDebug
- type SpaceInfo
- type SpaceListEvent
- type SpaceSyncStatus
- type Stamp
- type Status
- type SubRecord
- type SubscriptionEvent
- type SyncState
- type SyncStatusAPI
- type TouchedRecord
- type TreeHeads
- type TypeCreateParams
- type TypeInfo
- type TypePatch
- type TypesAPI
- type UnreadChange
- type UpsertBatch
- type UpsertRecord
- type UpsertRejection
- type UpsertResult
- type Variant
- type Version
- type VersionId
Constants ¶
const ( // TypeMarker is the `any.type` value of a type object. TypeMarker = "__type__" // CollectionMarker is the `any.type` value of a collection object. CollectionMarker = "__collection__" )
Membership vocabulary on the objects row — the fields Objects().Get returns and queries filter on:
any.type the object's one type id; "__type__" on a type
object, "__collection__" on a collection object
any.collections the ids of the collections the object is filed under
A filter `{"any.type": typeId}` returns the objects of that type and never the type object itself; `{"any.collections": collectionId}` the members of that collection and never the collection object.
const ( // SpaceTypeAny is the type of every created or derived space. // Used when CreateRequest.SpaceType is empty (the only accepted // value). SpaceTypeAny = "any.space" // SpaceTypeOneToOne is for derived 1-1 spaces shared between two // identities. The type is content-addressed into the symmetric // derived id, so both peers must use the same value — 1-1s pair // only within a product, and the any.* variant makes an // any↔anytype 1-1 structurally impossible. SpaceTypeOneToOne = "any.onetoone" )
On-the-wire SpaceType strings stamped into the space header at derive/create time. The any-sync-coordinator gates inbound space changes against an allow-list (see any-sync-coordinator spacestatus/changeverifier.go); anything outside it is rejected with "unknown space type: <value>" and headsync fails. The type is content-addressed into the immutable header, so a rejected value bricks the space permanently.
The SDK emits only the any.* family (the coordinator requires fileproto v2 in headers carrying it). anytype.* spaces belong to anytype-heart clients; the SDK can join/track them but never mints them.
const ( // ScopeSynced: written through the object's own CRDT, synced to // everyone with space access. The default. ScopeSynced = handler.ScopeSynced // ScopeDerived: SDK-stamped (author / createdAt / …), read-only. // Reserved for built-ins — user definitions cannot declare it. ScopeDerived = handler.ScopeDerived // ScopeAccount: synced across this account's devices only, via the // private tech space; invisible to other space members. ScopeAccount = handler.ScopeAccount // ScopeLocal: this device only; never synced. ScopeLocal = handler.ScopeLocal )
const ( MutableNever = handler.MutableNever MutableByAuthor = handler.MutableByAuthor MutableByAnyone = handler.MutableByAnyone StampNone = handler.StampNone StampCreator = handler.StampCreator StampCreateTime = handler.StampCreateTime StampModifyTime = handler.StampModifyTime IdAuto = handler.IdAuto IdUser = handler.IdUser DeleteByAnyone = handler.DeleteByAnyone DeleteByAuthor = handler.DeleteByAuthor )
const CRDTVersion = 2
CRDTVersion is the version of the CRDT data model this SDK writes and the newest one it can serve. The tech space records it (the `crdtVersion` record on the space-index object) the first time an SDK of this version opens the account, and the value never decreases — a monotonic rule every replica enforces on apply. An SDK that finds a higher stored version refuses to open (ErrCRDTVersionNewer); one that sees the higher version arrive while running keeps serving reads and refuses every user-authored synced write with the same error (SDK.CRDTVersion reports the state). Bump it when a release writes data the previous release cannot read or would corrupt by writing.
2: one type per object — `any.type` and `any.collections` replace the `any.types` list; a release that reads the list sees every object as typeless.
const IdentityProfileKind = "anysync-sdk-profile"
IdentityProfileKind is the identityRepo `Kind` string the SDK uses for the account profile record. The SDK's record is its own format — a NUL-separated layout (name\x00description\x00iconCID) encrypted with the account metadata symkey and signed by the account key — namespaced apart from other clients' profile kinds, whose encrypted-protobuf format it can't decode without explicit negotiation.
The kind string MUST NOT contain a dot — the coordinator stores records as MongoDB sub-documents keyed by Kind (`data.<kind>`), so a dot is interpreted as a nested-path separator and silently re-shapes the record on disk.
const RecordsModule = "records"
RecordsModule is the built-in generic module: a schema-enforced dataset with no shared collection, always namespaced.
Variables ¶
var ( // ErrDuplicateInvite is returned by CreateInvite when an active // invite of the same type already exists. ErrDuplicateInvite = list.ErrDuplicateInvites // ErrInsufficientPermissions is returned by ACL ops the caller's // permission does not allow. ErrInsufficientPermissions = list.ErrInsufficientPermissions // ErrAclRecordNotFound is returned by ACL ops addressing a record // (or pending request) the ACL does not hold. ErrAclRecordNotFound = list.ErrNoSuchRecord )
Re-exported any-sync ACL sentinels, so consumers classify ACL failures with errors.Is against this package instead of importing any-sync internals.
var ( // ErrAggGroupLimitExceeded — too many unique $group keys. ErrAggGroupLimitExceeded = anystore.ErrGroupLimitExceeded // ErrAggAccumArrayLimitExceeded — a $push/$addToSet array grew past // the limit. ErrAggAccumArrayLimitExceeded = anystore.ErrAccumArrayLimitExceeded // ErrAggMemoryLimitExceeded — the blocking stages ($group state, // in-pipeline $sort) exceeded their retained-bytes budget. ErrAggMemoryLimitExceeded = anystore.ErrAggMemoryLimitExceeded )
Aggregation limit sentinels, aliased from any-store so errors.Is works against either spelling. All three surface mid-iteration (from Iterator.Err / All / Count), not from Iter itself.
var ( // ErrBundleUnknown — no live record for the bundle id. ErrBundleUnknown = errors.New("bundle unknown") // ErrBundleBadRequest — structurally invalid input: empty bundle // id, no root strategy, both strategies, RootType / RootCollections // / RootProperties next to NewRoot, RootType next to a declaration, // a seeded value that cannot be encoded, an invalid or duplicate // part / dataset / property declaration, Parts or Layout on a // Collection declaration, metadata without a declaration, or a // tech-space request with NewRoot or without a declaration. ErrBundleBadRequest = errors.New("bundle bad request") // ErrBundleNotLoser — ResolveLoser target is not a loser of the // bundle: it is the current winner, or was never claimed in roots. ErrBundleNotLoser = errors.New("bundle root is not a loser") // ErrLoserNotSynced — ResolveLoser target's tree has not synced to // this device yet (deletion needs the local head entry). Retry // after sync, or resolve from a device that holds the tree. ErrLoserNotSynced = errors.New("bundle loser tree not synced locally") // ErrBundleRootNotSynced: the registry references a root whose tree // (or objects row) has not arrived on this device yet. Transient — // retry after sync. ErrBundleRootNotSynced = errors.New("bundle root not yet synced locally") )
Bundles-registry sentinels — wrapped by the BundlesAPI methods so callers can classify with errors.Is.
var ( // ErrDeviceBadApp rejects an app slug that is empty or contains a // '.' (slugs are single-level path segments under apps. / // activeClaims.). ErrDeviceBadApp = errors.New("invalid device app slug") // ErrDeviceBadValue rejects a non-scalar app info value (string / // bool / number only, stored as float64 — same vocabulary as // settings). ErrDeviceBadValue = errors.New("unsupported device app value") // ErrDeviceEmptyUpsert rejects a SetDevice call with nothing to // write. ErrDeviceEmptyUpsert = errors.New("device upsert is empty") // ErrDeviceUnknown is returned by DeleteDevice when peerId has no // live row. ErrDeviceUnknown = errors.New("unknown device") // ErrDevicePruned reports a write absorbed by the own row's sticky // tombstone: this device was pruned (DeleteDevice) and its peer id // can never re-register. Without this error the absorbed write // would be indistinguishable from success. ErrDevicePruned = errors.New("device row is pruned") // ErrDeviceSelfDelete rejects DeleteDevice on the local device's // own row — the tombstone is sticky, so self-pruning would // permanently lock this installation out of the registry. Prune a // device from one of the account's other devices instead. ErrDeviceSelfDelete = errors.New("cannot delete own device row") )
Devices-registry sentinels — wrapped by the Service device methods so callers can classify with errors.Is.
var ( // ErrFileNotAvailable — the file's content is neither local nor // fetchable right now: the file is not backed up yet (the P2P rung // lands later), the network advertises no public read base, or a // read hit a not-yet-fetched range while offline. ErrFileNotAvailable = errors.New("space: file content not available") // ErrFileNotBackedUp — Offload refused: the local bytes are the // only copy of a file whose backup hasn't completed, and the SDK // never drops the only copy. ErrFileNotBackedUp = errors.New("space: file not backed up") // ErrFileVariantInvalid — Attach variant options are inconsistent: // Variant/VariantOf not set together, or the original is bound to // a different object. ErrFileVariantInvalid = errors.New("space: invalid file variant options") )
Typed file errors — match with errors.Is, never by message.
var ( // ErrHistoryTruncated — the causal past walks off the locally // available history (snapshot horizon or ACL gap). Version history // is best-effort-depth by contract, never a durability promise. ErrHistoryTruncated = errors.New("space: history: earlier changes not available on this device") // ErrViewTooLarge — the requested cut materializes more state than // a view may hold; narrow the scope (dataset / record). ErrViewTooLarge = errors.New("space: history: view too large — narrow the scope") // ErrVersionNotFound — the version (ChangeId) is not present in // this device's tree storage. ErrVersionNotFound = errors.New("space: history: version not found") )
History-surface errors.
var ( // ErrPubSubInvalidTopic — the topic or pattern is malformed: // segments are `/`-separated, non-empty, at most 16 per topic and // 256 bytes total; wildcards (`*`, `>`) are valid in patterns only. ErrPubSubInvalidTopic = errors.New("space: invalid pubsub topic or pattern") // ErrPubSubPayloadTooLarge — the payload exceeds the network's // per-message cap (64 KiB by default). ErrPubSubPayloadTooLarge = errors.New("space: pubsub payload too large") // ErrPubSubTopicNotOwned — the topic is in the reserved self-owned // `acc/…/<accountId>` namespace of another account. ErrPubSubTopicNotOwned = errors.New("space: pubsub topic owned by another account") // ErrPubSubNoReadKey — this identity has no read key for the space // (keyless reader, or a guest-mode space), so it can neither encrypt // a publish nor decrypt a subscription. ErrPubSubNoReadKey = errors.New("space: no read key for pubsub") // ErrPubSubTooManyPatterns — the per-space subscription pattern cap // (100 by default) is exhausted. ErrPubSubTooManyPatterns = errors.New("space: too many pubsub patterns") )
PubSub errors. Publish/Subscribe wrap these sentinels; match with errors.Is.
var ( ErrSettingsEmpty = errors.New("settings patch is empty") ErrSettingsBadKey = errors.New("invalid settings key") ErrSettingsBadValue = errors.New("unsupported settings value") ErrSettingsKeyOverlap = errors.New("settings key in both set and unset") )
Settings-patch sentinels — wrapped by SetSettings validation errors so callers can classify with errors.Is.
var ( ErrNotAType = errors.New("space: the id names a collection, not a type") ErrNotACollection = errors.New("space: the id names a type, not a collection") )
ErrNotAType is returned by the type-only surface (Parts / AddPart / AddDataset / Patch / … on TypesAPI) when the id names a collection object; ErrNotACollection by CollectionsAPI.Patch when the id names a type object. The shared property-definition methods accept either. Client errors → 4xx.
var ErrBadIdentity = errors.New("bad identity")
ErrBadIdentity is returned by identity-taking methods (ACL ops, OneToOne, RegisterIncoming) when the identity string is empty or not a decodable account address.
var ErrBadPipeline = errors.New("space: aggregate: bad pipeline")
ErrBadPipeline wraps every pipeline normalization/parse/validation failure surfaced by Agg terminals — a malformed JSON pipeline, a non-array pipeline, an unknown stage or accumulator, or a $text / vector clause outside the pushdown prefix. errors.Is-able.
var ErrBadSpaceId = errors.New("invalid space id")
ErrBadSpaceId is returned by Track when the given id does not have the any-sync spaceId shape (`<cid>.<replication key base36>`). A malformed id would otherwise sit in the index and fail every Get with an opaque remote error — Track rejects it up front instead.
var ErrBadSpaceType = errors.New("unsupported space type")
ErrBadSpaceType is returned by Create when CreateRequest.SpaceType is outside the allow-list (SpaceTypeAny or empty). The type is content-addressed into the immutable space header and coordinator- gated, so a bad value is rejected up front — classify with errors.Is to turn it into a caller-facing 4xx.
var ErrCRDTVersionNewer = errors.New("space: account CRDT version is newer than this SDK supports")
ErrCRDTVersionNewer: the account's data was written by a newer SDK than this one — the tech space carries a CRDT version above CRDTVersion. Returned by Open and, once the higher version arrives at runtime, by every synced write. errors.As to CRDTVersionNewerError for the versions.
var ErrDatasetNotDeclared = errors.New("space: dataset is declared by none of the object's types")
ErrDatasetNotDeclared is returned by the write surface (Modify / ModifyMany / Delete / Upsert) when the target object carries no type whose parts declare the dataset — a namespaced collection's one owner, or any owner of a module's canonical collection. No type is attached on write; the caller attaches one first. A client error → 4xx.
var ErrGuestJoinPending = errors.New("space: guest join recorded; space load pending")
ErrGuestJoinPending is returned by Service.JoinGuest when the guest row was recorded durably but the space content isn't pullable yet. Loading continues in the background and across restarts; callers poll List/Get or Subscribe for the flip to StatusActive.
var ErrImmutableFieldChanged = errors.New("space: upsert would change an immutable field")
ErrImmutableFieldChanged marks an upsert record whose payload would change a write-once field on an existing record. The record is rejected explicitly (never silently skipped — that would hide caller data loss); the rest of the page proceeds.
var ErrInvalidFieldValue = errors.New("space: invalid dataset field value")
ErrInvalidFieldValue is returned by PatchDataset when a MUTABLE leaf's value is malformed (e.g. a `search.text` mapping with empty or duplicate keys, or an empty spelling where Unset is the clear path) — distinct from ErrPinnedField, which reports an immutable PATH. Consumers map it to a validation-class client error.
var ( // ErrInvalidInvite is returned by DecodeInvite when the input is // not a base58-encoded invite produced by EncodeInvite. ErrInvalidInvite = errors.New("anysyncsdk: invalid invite") )
var ErrInviteAcceptPending = errors.New("invite accepted; space load pending")
ErrInviteAcceptPending is returned by AcceptInvite when the accept was recorded (synced account-wide) but the space content is not pullable yet. Loading continues durably in the background and across restarts; callers poll List/Get or Subscribe for the flip to StatusActive.
var ErrIsDerivedSpace = errors.New("derived spaces cannot be deleted")
ErrIsDerivedSpace is returned by Delete when the target is a seed-derived space (created via Derive). Derived spaces are permanent: the deterministic id means a delete followed by a re-derive would recreate the space with fresh history under the same id — history replacement — and the sticky deleted tombstone would otherwise wedge the account's well-known derived id forever. The deriving account's row carries a synced set-once `derived` flag (surfaced as SpaceInfo.Derived) that every enforcement point keys on; a joiner of someone else's derived space never gets the flag — they cannot re-derive it, so their removal stays allowed. 1-1 spaces keep their own re-derivable delete path.
var ErrIsOneToOne = errors.New("is a 1-1 space")
ErrIsOneToOne is returned when a regular-space invite op targets a 1-1 space — use the OneToOne accept/decline methods instead.
var ErrIsTechSpace = errors.New("cannot track the tech space")
ErrIsTechSpace is returned by Track when the given id is the account's own tech space — the tech space is system-owned and never appears in the space list.
var ErrJoinNotPending = errors.New("no pending join request")
ErrJoinNotPending is returned by CancelJoin when there is no pending join request to withdraw: the row is not StatusJoining, or the ACL no longer holds the request because the owner already accepted it while the cancel was in flight (the space loads and the row reaches StatusActive — poll List / Subscribe), or a fresh request is on the chain that the first snapshot missed (the join controller settles it). Two outcomes are NOT this error: a request gone with no membership behind it — CancelJoin marks the row ended and returns nil — and a chain that could not be read, reported as the transport error with the row untouched.
var ErrJoinPending = errors.New("join pending owner approval")
ErrJoinPending is returned by Join after the RequestToJoin was posted but the owner has not yet accepted. The space is recorded in the index with StatusJoining; callers poll List for the status flip and then call Get.
var ErrModuleOwned = errors.New("space: dataset schema is owned by its module")
ErrModuleOwned is returned by the field-level dataset methods (AddDatasetField / RemoveDatasetField / PatchDatasetField) when the dataset is served by a module: the module owns the schema, so a declaration carries no fields. A client error → 4xx.
var ErrModuleReserved = errors.New("space: module is reserved for the consumer's own installs")
ErrModuleReserved is returned by AddPart / AddDataset and by BundlesAPI.Ensure when a dataset draft names a module registered with handler.Module.Reserved: only the consumer's own installs (the SystemInstall ensure option) may declare it. A client error → 4xx.
var ErrNoActiveGuestKey = errors.New("no active guest key")
ErrNoActiveGuestKey is returned by RevokeGuestKey when the space has no guest identity to revoke.
var ErrNotFound = errors.New("space: not found")
ErrNotFound is returned by Query.One when the query produced no match. Other lookups (Get, etc.) return their own dedicated errors.
var ErrNotInvitePending = errors.New("not invite-pending")
ErrNotInvitePending is returned by AcceptInvite / DeclineInvite when the space is not awaiting direct-add invite approval.
var ErrObjectDeleted = errors.New("space: object deleted")
ErrObjectDeleted is returned by ObjectService.Get for an object whose tree any-sync records as deleted — here or on a peer, and by Derive for a ParentId in that state. Distinct from ErrNotFound: the id existed and is gone for good.
var ErrObjectNotFound = errors.New("space: object not found")
ErrObjectNotFound is returned by a per-object operation that has to open the object's tree — record reads, writes, history — when this device has no such tree: the id is unknown here, or the object was deleted. One sentinel for both because the caller can act on neither — the object is not addressable on this device.
Derive with a ParentId returns it for a parent not yet held here (see DeriveObjectOpts.ParentId); a retry after the parent syncs succeeds.
Subscribe reports it for a DELETED object but not for an unknown one: an id with no tree here yields an empty initial snapshot instead, so a subscription registered before the object lands still receives its events. Handle the sentinel on the subscribe path too.
Distinct from ErrObjectDeleted, which ObjectService.Get raises for the narrower "this id existed and is gone for good"; a consumer that needs the distinction reads the row through Get.
var ErrPinnedField = errors.New("space: property field is pinned or immutable")
ErrPinnedField is returned by PatchProperty / PatchDataset / PatchDatasetField when a Set/Unset path targets pinned state (key, kind, scope, items, properties; a dataset head's behavioral fields). Consumers (e.g. the `any` server) match it to surface a clean 400 rather than the handler's per-op drop.
var ErrPushNotConfigured = errors.New("space: push notifications not configured (no push peer in config)")
ErrPushNotConfigured is returned by every PushAPI method when the SDK was opened without a push node (config.Push empty). The API handle itself is always non-nil — configuration is checked at call time, not at Open.
var ErrReadOnlySpace = errors.New("space: read-only")
ErrReadOnlySpace rejects synced writes into a space this account cannot write to: any space opened via a guest key, and any space where the ACL grants a role without write permission (reader / guest). The nodes would drop the records anyway — the SDK fails the write up front with a typed error instead of letting local state silently diverge or surfacing a low-level ACL rejection.
var ErrReadTrackingDisabled = errors.New("space: read tracking not enabled")
ErrReadTrackingDisabled is returned by ReadStateAPI methods when no dataset in the space opted into read tracking.
var ErrRecordDeleted = crdt.ErrRecordDeleted
ErrRecordDeleted is the ReasonErr (match with errors.Is) of the whole-record rejection emitted when a modify or upsert lands on a tombstoned record. Record deletion is sticky (CRDT delete-wins): the write is absorbed, nothing is stored, and the id can never be reused — without this rejection the absorbed write would be indistinguishable from a successful create. Deleting an already-deleted record stays silent (idempotent).
var ErrSelfPair = errors.New("cannot pair with self")
ErrSelfPair is returned by OneToOne / RegisterIncoming when the given identity is the caller's own account.
var ErrSpaceDeleted = errors.New("space: deleted")
ErrSpaceDeleted is returned by Service.Get for a space whose index row carries a deletion marker (local or synced, including the 1-1 offload marker), and by Join when the row is a synced tombstone (a device-local ended join is not — Join revives it). The storage is offloaded — loading would SpacePull a space the account removed, which the nodes reject anyway. A deleted 1-1 is re-creatable via Service.OneToOne, an ended join via Join; the synced tombstone Delete writes is terminal.
var ErrSpaceNotAccepted = errors.New("space: not accepted; not materialized")
ErrSpaceNotAccepted is returned by Service.Get for a space this account knows of but has not accepted yet: a pending join (StatusJoining), an incoming or declined 1-1 (StatusOneToOnePending / StatusOneToOneDeclined), or a pending/declined direct-add invite. Nothing is downloaded for such spaces — a load with no local storage triggers any-sync's SpacePull bootstrap, so materializing a pending row would pull the whole space ciphertext before the user accepted it. Acceptance (owner approval of a join, AcceptOneToOne, AcceptInvite) is what authorizes materialization.
var ErrSpaceNotTracked = errors.New("space: read state not tracked for space")
ErrSpaceNotTracked is returned by MarkRead / MarkReadUpTo when the space's read state is not tracked on this device at mark time: the space is unknown, deleted, or pending (join / incoming 1-1) in the tech-space index — e.g. a mark racing a concurrent delete.
var ErrSpaceUnknown = errors.New("unknown space")
ErrSpaceUnknown is returned by id-addressed Service methods (Get, Delete, SetSettings, the accept/decline families) when the spaceId has no row in the account's space index.
var ErrSubscribeUnsupported = errors.New("space: live subscription not supported for this query target")
ErrSubscribeUnsupported is returned by Query.Subscribe for query shapes the engine doesn't support yet (members in v1).
var ErrSubscriptionDrifted = errors.New("space: subscription drifted (too many records left the held window); resubscribe required")
ErrSubscriptionDrifted is returned by QuerySubscription.Err when the engine has lost more than QueryOpts.DriftBudgetPercent of the held window without replacements. The engine never re-queries any-store on the hot path to backfill — it closes the subscription and the client is expected to resubscribe.
var ErrSubscriptionOverflow = errors.New("space: subscription overflowed; resubscribe required")
ErrSubscriptionOverflow is returned by QuerySubscription.Err when the per-sub mailbox filled before the consumer could drain it. The engine drops the rest of the batch and closes the mailbox. The client should resubscribe to recover.
var ErrTypeRegistered = errors.New("space: definition is registered — its declarations are static")
ErrTypeRegistered is returned by the runtime mutators (AddProperty / RemoveProperty / PatchProperty / Patch and the part methods) when the target type or collection is a registered built-in whose declarations are static and cannot be mutated at runtime. A client error → 4xx.
var ErrTypeRequired = errors.New("space: an object needs a type")
ErrTypeRequired is returned by Objects().Create without a Type, by Derive when the object has no type and none is given, and by a raw write that clears `any.type`: every object has exactly one type. A client error → 4xx.
var ErrUnsupported = errors.New("space: unsupported on this space")
ErrUnsupported is returned by every surface the tech-space handle (Service.Get(techSpaceId)) does not offer: object and type lifecycle, members and ACL, files, history, read state, pub/sub, change feed, metadata, and generic writes to anything but a bundle dataset. Classify with errors.Is. Subscribe-style methods whose signature carries no error (Members / Files / ReadState / Changes) are inert there: a no-op cancel, a callback that never fires.
var ErrUpsertNotAuthor = errors.New("space: upsert would edit another author's field")
ErrUpsertNotAuthor marks an upsert record whose payload changes an author-mutable field on a record this account did not create. Checked client-side against the stored creator stamp so the rejection is deterministic instead of an apply-time surprise.
var ErrUpsertRequiresUserIds = errors.New("space: upsert requires an id:user dataset")
ErrUpsertRequiresUserIds: Upsert only serves datasets declared with IdUser — the caller-supplied record id is the idempotency key the diff runs on. Auto-derived-id datasets have no stable caller-side identity to upsert against.
var ErrWrongSlot = errors.New("space: a type goes in any.type, a collection in any.collections")
ErrWrongSlot is returned by the membership writes (SetType / AttachCollection / Objects().Create / Derive / Bundles().Ensure) and by a raw Modify on the objects row when a known collection id is set as `any.type` or a known type id is added to `any.collections`. An id this device cannot resolve passes. A client error → 4xx.
Functions ¶
func ActiveDevice ¶
ActiveDevice resolves which device is the active instance of app — THE single implementation of the election rule (SYN-165's top risk is two consumers deciding differently; UI, runtime, and the `any` server must all call this, never re-derive it).
Deterministic on converged data for every reader: among live rows that carry the app installed (Apps[app] present — a dangling claim on a device that uninstalled the app never wins; a pruned device has no row at all), the claim with the highest Seq wins, ties broken by highest At, then by lexicographically largest peer id. ok=false when no device qualifies.
A claim with Seq <= 0 is treated as absent: ClaimActive mints seqs from 1, so a zero can only come from a malformed bag (unknown future writer, corrupt data) that decoded to the zero value — it must never beat genuinely-unclaimed rows.
Known limit (v1): Seq is minted from the claiming replica's view, so a claim made on a not-yet-synced device can mint a lower Seq than an unseen earlier claim and lose once heads converge — the user's newest intent losing to an older one. Claims are cheap: re-claim after sync.
func CompareVersion ¶
CompareVersion returns -1, 0, or +1 for a < b, a == b, and a > b. The empty string is the "no version" sentinel and sorts less than any non-empty version.
func DeriveAccountMetadataSymKey ¶
DeriveAccountMetadataSymKey deterministically derives an account's metadata symmetric key from its private account (sign) key. The key is shared with contacts through already-encrypted channels (a shared space's ACL metadata, a 1-1 invite) so they can decrypt this account's identityRepo profile; see docs/one-to-one-spaces.md and EncryptProfile.
func EncodeAccountMetadata ¶
func EncodeAccountMetadata(m AccountMetadata) []byte
EncodeAccountMetadata serialises an AccountMetadata into the canonical bytes the SDK pushes to identityRepo. Same NUL-separated layout as the join-time metadata blob (see internal/spaceimpl encodeMetadata) so the same decoder works on both sides — keeps the parsing surface small.
Returns nil for an empty input; caller is expected to refuse-to-push on nil to avoid clobbering an existing profile with empty values.
func EncodeInvite ¶
EncodeInvite packs i into the share-friendly base58 string. The inverse is DecodeInvite.
Layout (pre-base58):
[1] version=1 [varint] len(spaceId) [..] spaceId bytes [..] proto-marshalled invite private key (rest of buffer)
func EncryptProfile ¶
func EncryptProfile(meta AccountMetadata, key crypto.SymKey) ([]byte, error)
EncryptProfile serialises an AccountMetadata and encrypts it with the account's metadata symkey for upload to identityRepo. Returns nil for empty input (caller should refuse-to-push on nil rather than clobber an existing profile). The plaintext layout is the same NUL-separated blob as the (now-deprecated plaintext) identityRepo format — only the transport is encrypted, so DecodeAccountMetadata still parses the decrypted bytes.
func MarshalSymKey ¶
MarshalSymKey encodes a symmetric key to its canonical string form for storage in the account-scoped identity→key cache. EncodeSymKey/ DecodeSymKey round-trip through this representation.
Types ¶
type ACL ¶
type ACL interface {
// CreateInvite mints a new RequestToJoin invite, revoking any
// prior invite on this space (mirrors AclSpaceClient.ReplaceInvite
// semantics — only one active invite at a time). The returned
// Invite is share-friendly (base58-encoded via Invite.Encode()
// equivalents on space.EncodeInvite).
CreateInvite(ctx context.Context) (Invite, error)
// RevokeInvite revokes a single invite by record id. The record id
// is what AclState lists in InviteIds.
RevokeInvite(ctx context.Context, inviteRecordId string) error
// RevokeAllInvites tears down every active invite in one batch.
RevokeAllInvites(ctx context.Context) error
// AcceptRequest approves a pending join request and grants the
// joiner the given permission. requestRecordId is the id from
// MembersAPI.JoinRequests. Owner / admin only.
AcceptRequest(ctx context.Context, requestRecordId string, perm Permission) error
// DeclineRequest rejects a pending join request from identity.
// Owner / admin only.
DeclineRequest(ctx context.Context, identity string) error
// ChangePermissions updates the permissions of one or more existing
// members in a single batch. Owner / admin only; cannot demote the
// owner.
ChangePermissions(ctx context.Context, changes []PermissionChange) error
// RemoveAccounts kicks one or more members out, rotating the read
// key so removed members can no longer decrypt new content. Owner
// / admin only.
RemoveAccounts(ctx context.Context, identities []string) error
// AddAccounts adds members directly without an invite/request
// round-trip — the whole batch lands in ONE ACL record. Useful
// wherever the caller already holds the joiners' identities.
//
// When the coordinator inbox transport is available, each added
// account is also notified durably (queued + retried across
// restarts): the space surfaces on their devices as
// StatusInvitePending for them to AcceptInvite / DeclineInvite.
// Without the transport (headless deployments) the ACL write still
// happens but no notification is sent.
AddAccounts(ctx context.Context, accounts []MemberAdd) error
// OwnershipChange transfers ownership to newOwner; the old owner's
// permission becomes oldOwnerPerm (typically PermissionAdmin).
OwnershipChange(ctx context.Context, newOwner string, oldOwnerPerm Permission) error
// RequestSelfRemove asks to be removed from this space. Self-service
// — works for any non-owner, non-guest member.
RequestSelfRemove(ctx context.Context) error
// CancelJoinRequest withdraws the caller's pending request on a
// LOADED space. A pending join is never loaded (Service.Get refuses
// it), so withdrawing a join request goes through the account-level
// Service.CancelJoin; this handle only reaches a request the loaded
// space can hold, i.e. a pending self-remove.
CancelJoinRequest(ctx context.Context) error
// StopSharing drops every non-owner member, revokes every invite,
// and rotates the read key in one batch. Owner only.
StopSharing(ctx context.Context) error
// CreateGuestKey enables public read-only access: it mints a shared
// guest identity, adds it to the ACL with PermissionGuest, and
// returns it as an InviteKindGuest invite for Service.JoinGuest.
// One active guest key per space; the private key is persisted on
// the owner's tech-space row, so repeated calls return the same
// invite while the guest identity is still active in the ACL
// (idempotent — the key is not recoverable from the ACL itself).
// Owner only: custody lives in the owner's tech space, so admins
// can neither fetch nor reissue it.
CreateGuestKey(ctx context.Context) (Invite, error)
// RevokeGuestKey removes the guest identity from the ACL and
// rotates the read key, cutting every guest off from new content
// (already-synced local copies stay readable on their devices).
// Clears the stored key; a later CreateGuestKey mints a fresh
// identity, so old invites die permanently. No-op error when no
// guest key is active.
RevokeGuestKey(ctx context.Context) error
}
ACL is the owner/admin-side ACL surface: invite lifecycle, join approvals, permission changes, ownership transfer, member removal. Mirrors any-sync's AclSpaceClient one-to-one with SDK-native types (identity strings instead of crypto.PubKey, Permission enum instead of list.AclPermissions).
Permissions: callers must hold the appropriate permission for each op (most ops are admin/owner-only). Failures surface as the underlying any-sync error.
Scope: only RequestToJoin invites are supported. AnyoneCanJoin is deferred until any-sync ships v2 of that invite type.
type AccountMetadata ¶
AccountMetadata is the owner/member metadata attached to ACL join records (identityRepo-backed on the wire).
func DecodeAccountMetadata ¶
func DecodeAccountMetadata(b []byte) AccountMetadata
DecodeAccountMetadata is the inverse — used by the per-space fetcher to interpret the bytes pulled from identityRepo. Returns the zero value on a malformed input rather than an error: receivers fall through to the join-time metadata snapshot in that case.
func DecryptProfile ¶
func DecryptProfile(data []byte, key crypto.SymKey) (AccountMetadata, bool)
DecryptProfile decrypts an identityRepo profile blob with key and decodes it. ok is false when key is nil, data is too short to be ciphertext, or decryption fails — callers must NOT fall back to parsing the raw bytes as plaintext (ciphertext parsed as a NUL blob yields a garbage name; a legacy plaintext record stays unresolved until a re-push overwrites it with ciphertext). A reader without the contact's symkey simply can't resolve the profile yet; it surfaces from identity alone until the key arrives (see docs/one-to-one-spaces.md, the identityMetaKeys cache).
type Agg ¶
type Agg interface {
// GroupLimit overrides the maximum number of unique $group keys
// (any-store default 50 000; negative = unlimited).
GroupLimit(n int) Agg
// AccumArrayLimit overrides the maximum $push / $addToSet array
// length (any-store default 10 000; negative = unlimited).
AccumArrayLimit(n int) Agg
// MemoryLimit overrides the retained-bytes budget shared by the
// pipeline's blocking stages (any-store default 256 MiB; negative =
// unlimited).
MemoryLimit(bytes int) Agg
// Iter executes the pipeline and streams result documents. Results
// are pipeline output, not dataset rows — a $group doc carries the
// group key as `id`, a $count doc is `{<name>: N}` with no id at
// all. Each value is valid only until the next Next() call.
Iter(ctx context.Context) (Iterator, error)
// All materializes every result into a slice, cloned off the
// iterator's reused buffers so caller-held pointers stay valid.
All(ctx context.Context) ([]*anyenc.Value, error)
// Count executes the pipeline and returns the number of result
// documents. (A terminal $count stage instead emits the count as a
// document.)
Count(ctx context.Context) (int, error)
// Explain returns the access plan of the pushed prefix plus the
// in-pipeline stage list. Diagnostic only, NOT a stable format —
// don't parse it.
Explain(ctx context.Context) (string, error)
}
Agg is the caller-facing aggregation builder, returned by Space.Aggregate / Space.AggregateObjects. It wraps any-store's MongoDB-style pipeline ($match / $sort / $skip / $limit / $count / $project / $addFields / $unwind / $group): the longest pushable prefix runs through the access planner (indexes, $text, vector), the rest streams in-pipeline. See any-store docs/aggregation.md for stage semantics and the deliberate MongoDB divergences.
Like Query, the builder is single-shot — call Space.Aggregate again per read. Snapshot-only: there is no live/subscribe variant.
Tombstoned rows are always excluded (a `_deletedAt missing` $match is prepended to the pipeline, so it stays in the pushdown prefix); there is no IncludeDeleted escape hatch. Deleted objects are purged rather than tombstoned, so they never appear here regardless; observe object deletions via QueryObjects().Subscribe (see ChangeIndexAPI). IndexHint is not exposed either; both are additive later if needed.
type AttachOpts ¶
type AttachOpts struct {
// Name is the user-facing file name.
Name string
// Mime is the content-type hint.
Mime string
// Variant + VariantOf attach this content as an alternate
// representation of an existing file on the SAME object (the
// embedder produces the bytes — e.g. a thumbnail it rendered).
// The variant is an ordinary sibling file with its own tier,
// durability and lifecycle; Open(originalId, variant) resolves it.
// Both must be set together; VariantOf must reference a file bound
// to the same objectId.
Variant Variant
VariantOf string
}
AttachOpts is caller metadata for one attached file. All fields ride inside the sealed (member-only) part of the payloads row.
type Bundle ¶
type Bundle struct {
// Id is the stable bundle identifier (marketplace id or a
// hardcoded slug like "bao/v1") — the record id.
Id string
// Name is the bundle's display name.
Name string
// RootId is the winning root object id — the converged LWW value.
// Children of the setup are derived from it (DeriveObjectOpts.
// ParentId), so this one id transitively names the whole install.
RootId string
// Roots is every root object id ever claimed for this bundle, in
// arrival order. Add-only audit trail — entries are never removed;
// a resolved loser's death is recorded by its tree deletion.
Roots []string
// Losers is the live conflict set: Roots minus the winner minus
// roots whose trees are already deleted. Non-empty means a
// concurrent-install conflict awaits resolution — merge what
// matters out of each loser, then ResolveLoser it.
Losers []string
// Derived reports that the winner is the bundle's canonical
// derived root (EnsureBundleRequest.DerivedRoot). Such an install
// cannot fork and cannot be uninstalled: the root id is a pure
// function of (space, bundle id), and derived trees are not
// deletable. Computed on read, not stored.
Derived bool
}
Bundle is one row of the bundles registry.
type BundlesAPI ¶
type BundlesAPI interface {
// Ensure installs the bundle or adopts the existing install:
// when a live record with a winner exists, it is returned as-is
// (no root is created); otherwise the root is minted — NewRoot for
// a created root, the canonical derivation for DerivedRoot — and
// one change registers it ($set rootId + $addToSet roots). Fully
// local — no network wait; two devices ensuring concurrently each
// register their root and the CRDT converges on one deterministic
// winner after sync, the other surfacing in Losers. Callers must
// therefore treat the returned RootId as provisional until the
// space has synced, and re-read after.
//
// Adoption wins over derivation: a bundle already installed on a
// created root stays on it, even when this call asks for
// DerivedRoot. Nothing migrates behind the caller's back — check
// Bundle.Derived to see what the install actually is.
//
// A CLAIMED CANONICAL DERIVED ROOT ALWAYS WINS the registry, on
// every replica, whatever the rootId register says. The claim set
// is add-only, so the verdict itself is order-independent: every
// replica reads the same winner from any prefix containing the
// claim, which is what keeps a derived root (undeletable, and
// therefore unresolvable as a loser) from ever becoming one.
//
// The verdict is not a race, but the CLAIM can be: a device that
// installs a derived root without a converged registry demotes an
// existing created install to a loser, irreversibly, on every
// replica. Converge before installing derived into a space that
// may already carry a created install of the same id — see
// docs/bundles.md § Derived roots.
//
// A declaring root (Parts, Properties or XKey; Collection for a
// collection) is stamped as a definition: the root's first change
// carries its membership (the marker in `any.type`, or RootType;
// RootCollections), `any.name`, the definition metadata (`xkey` /
// `layout` / `hidden` under `type` or `collection`) and the
// RootProperties values together — one `objects` change — then the
// registry row, then the declarations (one `properties` change,
// one `datasets` change for a type): root + up to 3 changes. A
// crash mid-install leaves a registered row the retry heals
// idempotently. Two devices declaring the same name concurrently
// converge on one definition after sync; a DefId read before
// convergence may change — look definitions up by name when
// evolving them.
//
// On the tech space (Service.Get(SDK.TechSpaceId())) roots are
// minted by Ensure only — NewRoot is refused — and a declaration
// (Parts, Properties or XKey) is required; both root strategies
// are available.
//
// The bool reports whether THIS call registered the install.
// False means an existing one was adopted — which for a derived
// root may still materialize its tree locally.
//
// Options carry what a request body must never say: SystemInstall
// admits a reserved module (handler.Module.Reserved) for the
// consumer's own install.
Ensure(ctx context.Context, req EnsureBundleRequest, opts ...EnsureOption) (Bundle, bool, error)
// Get returns the bundle row. ErrBundleUnknown when no live record
// exists OR the winning root's tree is deleted — a dead winner
// reads as uninstalled everywhere (Get, List, Ensure's adopt gate),
// and the next Ensure reinstalls with a fresh root. As of local
// state — sync first for a network answer.
Get(ctx context.Context, bundleId string) (Bundle, error)
// List returns every live bundle row.
List(ctx context.Context) ([]Bundle, error)
// DerivedRootId is the id the bundle's derived root has in this
// space — a pure function of (space, bundle id), computed without
// reading the registry, materializing anything, or touching the
// network. Every device and every member gets the same answer,
// offline, which is what makes DerivedRoot installs fork-proof.
//
// It answers "where would this bundle live", not "is it
// installed": a bundle installed on a created root lives
// elsewhere, and an uninstalled one lives nowhere yet.
DerivedRootId(ctx context.Context, bundleId string) (string, error)
// ResolveLoser deletes a losing root object (cascade-deleting its
// derived children) after the caller has merged whatever content
// mattered out of it. The target must be a claimed root and must
// not be the current winner (ErrBundleNotLoser). Idempotent: a
// root already deleted returns nil. Deletion needs the loser's
// tree synced to this device — until then the call fails with
// ErrLoserNotSynced (retry after sync, or run from the device that
// created the loser). Never auto-invoked — loser cleanup is always
// an explicit caller decision.
ResolveLoser(ctx context.Context, bundleId, loserRootId string) error
}
BundlesAPI is the typed surface over the per-space bundles registry. Obtained from Space.Bundles().
type CRDTVersionNewerError ¶
CRDTVersionNewerError carries the versions behind ErrCRDTVersionNewer.
func (*CRDTVersionNewerError) Error ¶
func (e *CRDTVersionNewerError) Error() string
func (*CRDTVersionNewerError) Unwrap ¶
func (e *CRDTVersionNewerError) Unwrap() error
type CRDTVersionState ¶
CRDTVersionState is the account's CRDT-version state: the version this SDK supports, the version the tech space records (0 until the first SDK carrying the mark opens the account), and whether the stored one is newer — in which case the SDK is read-only.
type ChangeIndexAPI ¶
type ChangeIndexAPI interface {
// MaxApplySeq returns the current upper bound of the cursor — the
// highest per-object applySeq persisted in this space. 0 when
// nothing has applied yet.
MaxApplySeq(ctx context.Context) (uint64, error)
// ChangedSince returns objects whose applySeq exceeds `since`,
// ordered ascending, capped at limit (0 = no cap). Page by passing
// the last returned ApplySeq as the next `since`.
//
// Objects already on disk before this SDK version started stamping
// space scope appear only after their next change ("index from now
// on" for pre-existing data).
ChangedSince(ctx context.Context, since uint64, limit int) ([]ObjectChange, error)
// Subscribe registers cb to fire once per applied change in this
// space. cb runs synchronously on the apply path — keep it small or
// hand work off to your own goroutine. The returned cancel is
// idempotent.
//
// Best-effort: a dropped event (crash, slow cb) is recovered by
// re-running ChangedSince from the consumer's persisted cursor. Do
// not treat the callback as a durable queue.
Subscribe(cb func(ObjectChange)) (cancel func())
// Generation is a per-space epoch that changes iff the SDK store was
// rebuilt (the applySeq axis renumbered). It is stable across normal
// restarts. A consumer persists it alongside its cursor; when the
// returned value differs from the stored one (or the stored cursor
// exceeds MaxApplySeq, an older-backup restore), it must reset the
// cursor to 0 and full-reindex from a live QueryObjects snapshot.
Generation(ctx context.Context) (string, error)
}
ChangeIndexAPI is the surface a consumer-side indexer (full-text / vector search, etc.) drives to track what changed in a space and re-index incrementally. The SDK ships no index of its own and stores no cursor — the consumer owns both.
Two paths that reconcile because they share applySeq ordering:
- Subscribe — best-effort live "this object is dirty" notifications.
- ChangedSince — durable catch-up: replay everything past a cursor.
A consumer persists its last-seen ApplySeq, reacts to Subscribe for liveness, and on startup (or after a missed event) calls ChangedSince from its saved cursor to backfill. Changes to any of the object's datasets count, whatever route they arrived on.
Object DELETION IS reported through this feed. A deleted object's projection is purged (the SDK keeps no `objects` tombstone; any-sync's head storage is the durable delete record), but the purge stamps the object's kept `_meta` row with a fresh ApplySeq, so it surfaces once as ObjectChange{Deleted:true} at an ApplySeq strictly greater than its last content change. The consumer reads change.Deleted and evicts. Once Deleted surfaces, any-sync guarantees no later content change follows, so per-object consumer state may be dropped.
Rebuild: applySeq is peer-local and renumbers if the SDK store is rebuilt (e.g. its DB was wiped). Generation changes exactly then; a consumer whose stored Generation differs MUST reset its cursor to 0 and full-reindex from a live snapshot (QueryObjects), which re-establishes deletions by absence.
type ChangeList ¶
type ChangeList struct {
Changes []ChangeMeta
// Cursor resumes the NEXT page; "" = history exhausted.
Cursor string
}
ChangeList is one ListChanges page.
type ChangeMeta ¶
type ChangeMeta struct {
Version Version // ChangeId; for groups: the head (newest) member
Author string // per-change signer identity (the objects row's modifiedBy, not its author)
Timestamp int64 // author clock, Unix seconds — display-only
Dataset string
TraceIds []string
Touched []TouchedRecord
// Truncated marks the oldest listable entry when the history
// horizon was hit. RESERVED: always false today — the SDK never
// writes tree snapshots, so full history is always local. It
// becomes meaningful with the future snapshot/GC contract
// (proposal §9); until then only ViewAt/Diff can surface
// ErrHistoryTruncated (ACL gaps).
Truncated bool
GroupSize int // 1 unless coalesced
}
ChangeMeta describes one listed change (or coalesced group).
type CoalesceOpts ¶
type CoalesceOpts struct {
// Window bounds the author-clock spread between adjacent group
// members. <=0 = 5 minutes.
Window time.Duration
}
CoalesceOpts tunes list-time grouping (proposal §7.1): consecutive same-author linear-chain changes within Window collapse into one entry whose handle is the group's newest ChangeId. Never across merges or (page-visible) branches.
type CollectionCreateParams ¶
type CollectionCreateParams struct {
Name string
Description string
IconCID string
XKey string
Hidden bool
Meta map[string]any
}
CollectionCreateParams is the input to CollectionsAPI.Create.
type CollectionInfo ¶
type CollectionInfo struct {
Id string
Name string
Description string
IconCID string
XKey string
Hidden bool
Meta map[string]any
// BuiltIn marks the synthetic `collection` meta-type and every
// caller-registered collection. User collections return false.
BuiltIn bool
}
CollectionInfo is a point-in-time snapshot of a collection object — the type-side subset that describes a definition rather than how its objects render: no layout, no parts. `collection.xkey` / `hidden` / `meta` hold XKey / Hidden / Meta.
type CollectionPatch ¶
type CollectionPatch struct {
Name *string
Description *string
IconCID *string
Hidden *bool
Meta map[string]any
}
CollectionPatch is the input to CollectionsAPI.Patch. Nil pointers keep the current value; an empty string clears a text field; Meta patches per key, a nil value unsets.
type CollectionsAPI ¶
type CollectionsAPI interface {
PropertyDefsAPI
// List returns the meta `collection` built-in, every registered
// collection and every user collection of this space. Hidden ones
// are included; the consumer filters.
List(ctx context.Context) ([]CollectionInfo, error)
// Get returns one collection; ErrNotACollection for a type id,
// ErrNotFound for anything else.
Get(ctx context.Context, collectionId string) (CollectionInfo, error)
// Create a new user-defined collection. Returns the new object's
// id.
Create(ctx context.Context, params CollectionCreateParams) (collectionId string, err error)
// Delete a collection. Objects listing it keep the reference
// (orphan), per docs §"read tolerance".
Delete(ctx context.Context, collectionId string) error
// Patch edits a user collection's display and listing metadata.
// Registered built-ins refuse (ErrTypeRegistered); a type id
// refuses (ErrNotACollection).
Patch(ctx context.Context, collectionId string, patch CollectionPatch) error
}
CollectionsAPI manages collection objects inside a space.
A collection is what an object is filed UNDER: property definitions and nothing else — no parts, no layout. An object belongs to any number of collections, listed in `any.collections`; a collection object itself carries the reserved marker `__collection__` in `any.type`. Values for an object live on the objects row under `<collectionId>.<propId>`, exactly like a type's. A query on `any.collections` returns members only — a collection object never lists its own id.
The property-definition methods are the same surface TypesAPI exposes (PropertyDefsAPI) and accept a type id as well.
type CreateObjectOpts ¶
type CreateObjectOpts struct {
// Type is the object's one type (`any.type`). Required: every
// object has exactly one type (ErrTypeRequired otherwise).
Type string
// Collections are the collections the object is filed under at
// birth (`any.collections`). Optional.
Collections []string
// InitialProperties seeds base-scope property values. Keyed by
// owner (the type or a collection) → propId → value. An owner the
// object does not have is rejected — name it in Type / Collections.
InitialProperties map[string]map[string]any
}
CreateObjectOpts is the input to ObjectService.Create.
type CreateRequest ¶
type CreateRequest struct {
Name string
Description string
IconCID string
// SpaceType is stamped into the space header at create time and
// gated by the any-sync-coordinator. Must be SpaceTypeAny or empty
// (same meaning). Anything else is rejected by Create with a clear
// error; passing an invalid type would otherwise produce a space
// the coordinator refuses to sync.
SpaceType string
}
CreateRequest is the input to Service.Create.
type DatasetDef ¶
type DatasetDef struct {
Id string // head record id, immutable
// Key is the slug inside the type; Collection the name reads and
// writes address (the module's canonical collection when Shared,
// `<typeId>_<key>` otherwise) — server-computed, never client-set.
Key string
Collection string
Module string
// PartId is the owning part's id.
PartId string
DisplayName string
Description string
Dynamic bool
IdRule IdRule
IdPattern string
IdMaxLen int
DeleteBy DeletePolicy
SkipHistory bool
Search *SearchFields
Fields []DatasetFieldDef
// Invalid marks a definition whose folded declaration fails
// validation (InvalidReason says why). Invalid definitions never
// register or accept data but stay listed so they can be repaired
// (AddDatasetField) or removed.
Invalid bool
InvalidReason string
}
DatasetDef is the compiled view of one runtime dataset definition.
type DatasetDefPatch ¶
DatasetDefPatch is the input to PatchDataset — same per-path model as PropertyPatch, over the dataset-def mutable leaves.
type DatasetDiff ¶
type DatasetDiff struct {
Dataset string
Records []RecordDiff
}
DatasetDiff groups record diffs of one dataset.
type DatasetDraft ¶
type DatasetDraft struct {
// Key is the dataset's slug inside its type — pinned. Namespaced
// datasets live in the collection `<typeId>_<key>`; a shared
// dataset's key is its module's canonical collection name and may
// be left empty to default to it.
Key string
// Module is the serving module — "records" (the default when
// empty), or a registered module such as "editor" / "chat".
Module string
// collection instead of a namespaced one: two types sharing the
// editor give an object carrying both a single body. Legal only
// for modules with a canonical collection; at most one shared
// dataset per module per type. Never for records.
Shared bool
DisplayName string
Description string
// Dynamic keeps a free-form keyspace next to the declared fields.
Dynamic bool
// IdRule / IdPattern / IdMaxLen: record-id production. Zero rule =
// auto-derived ids; IdUser accepts caller ids (also the upsert
// idempotency key) constrained by pattern/length.
IdRule IdRule
IdPattern string
IdMaxLen int
// DeleteBy gates record deletes. DeleteByAuthor requires a
// StampCreator field among Fields.
DeleteBy DeletePolicy
// SkipHistory keeps the dataset out of the version-history index.
SkipHistory bool
// Search is the optional search-extraction annotation (x-search).
Search *SearchFields
// Fields are the initial field definitions. Records datasets only —
// a module owns its schema and refuses fields.
Fields []DatasetFieldDraft
}
DatasetDraft is the input to TypesAPI.AddDataset (and PartDraft's Datasets).
type DatasetFieldDef ¶
type DatasetFieldDef struct {
// Id is the field definition record's id — the identity
// RemoveDatasetField / PatchDatasetField target.
Id string
Key string
Name string
Description string
Kind PropertyKind
// Shape is the full declared value shape (kind plus items /
// properties); Kind is its top-level kind.
Shape *handler.FieldShape
Scope Scope
Required bool
MutableBy Mutability
Stamp Stamp
// XFormat is the opaque descriptor declared on the field, nil when
// unset. See DatasetFieldDraft.XFormat.
XFormat map[string]any
}
DatasetFieldDef is the compiled view of one dataset field.
type DatasetFieldDraft ¶
type DatasetFieldDraft struct {
// Key is the on-record field name — pinned.
Key string
Name string
Description string
// Kind is the value kind. Required unless Stamp implies one
// (creator ⇒ string, createTime/modifyTime ⇒ datetime).
Kind PropertyKind
// Shape optionally refines array/object values (items/properties).
Shape *handler.FieldShape
// Scope: zero = synced. Derived is implied by Stamp and rejected
// otherwise.
Scope Scope
// Required: must be present on create. Incompatible with Stamp.
Required bool
// MutableBy: post-create write rule. Zero = write-once.
MutableBy Mutability
// Stamp: apply-time derived value (handler-written).
Stamp Stamp
// XFormat is the field's opaque descriptor (semantic slug, icon,
// options, …) — the same bag a property definition carries. Stored
// verbatim, mutable via PatchDatasetField, surfaced by Datasets()
// and as the `x-format` keyword in discovery. Nil when unset.
XFormat map[string]any
}
DatasetFieldDraft is one field definition — input to AddDataset / AddDatasetField.
type DatasetSchema ¶
type DatasetSchema struct {
Name string
JSONSchema json.RawMessage
// Owners are the types that declare the dataset: exactly one for a
// registered-type or namespaced dataset, every type declaring a
// shared dataset of the module for a canonical collection (empty
// while nothing declares it), none for space-level built-ins.
// Consumers gate indexing/eviction on it — an object may hold the
// dataset when it carries one of the owners.
Owners []string
// Module is the serving module ("records" for the generic
// schema-enforced kind, "editor" / "chat" for registered modules);
// empty for built-ins and registered-type datasets. Shared marks a
// module's canonical collection.
Module string
}
DatasetSchema describes one dataset's fields as a standard JSON Schema document, for consumer discovery (Space.Datasets / Service.Datasets).
JSONSchema is a JSON Schema object:
{"type":"object",
"properties":{"<field>":{"type":"string","title":"…","x-scope":"synced"}},
"additionalProperties":<dynamic>}
The `x-scope` extension keyword carries each field's class:
- "synced" — user/DAG-written, synced across the account's devices;
- "derived" — handler-computed, read-only to writers;
- "local" — device-local, never synced.
`additionalProperties:true` marks a dynamic dataset (free-form keys, e.g. the per-type object properties), where undeclared fields are allowed and treated as synced.
Behavioral declarations ride further x-keywords: per-field `x-mutable-by` / `x-stamp`, dataset-level `required`, `x-delete-by`, `x-id` (+ `x-id-pattern` / `x-id-max-length`), and `x-search` ({title,text} field mapping for external indexers; `text` is a bare field key or an array of keys — a single key marshals as the bare string).
type DebugAPI ¶
type DebugAPI interface {
Object(ctx context.Context, objectId string) (ObjectDebug, error)
Space() SpaceDebug
}
DebugAPI exposes a read-only diagnostic surface for one space. Obtained via Space.Debug(). Not a stable interface — fields and methods may grow or move as debug needs evolve. Production UI code should use SyncStatusAPI instead.
Two reads:
- Object(id) — per-object snapshot (tree structure + sync state
- local CRDT state). Triggers cold-restore on first touch of an object; not a hot path.
- Space() — per-peer headsync counters from the last diffsyncer round (in-memory, lost on restart).
type DeleteBatch ¶
DeleteBatch produces sticky tombstones for the listed record ids. Ignores RecordModify.Upsert semantics — tombstones always win and are always created, including for records that never existed locally (seeds a tombstone to preserve "delete wins absolutely").
type DeletePolicy ¶
type DeletePolicy = handler.DeletePolicy
Behavioral dataset-schema vocabulary, aliased from the handler package (one vocabulary for compiled-in and runtime declarations).
type DeriveObjectOpts ¶
type DeriveObjectOpts struct {
Seed []byte
// Type is set on first materialization; a type the object already
// has is never replaced. Required unless the object already has one
// (ErrTypeRequired). Collections it lacks are added on every call
// ($addToSet, idempotent); optional.
Type string
Collections []string
// ParentId derives the object as a child bound to this parent. The
// parent id is hashed into the child's derived id, so a child is
// re-derivable only with the same ParentId. Deleting the parent
// cascade-deletes the child's tree and excludes it from cold sync.
// Empty derives a top-level object.
//
// Creating the child needs the parent's tree on this device:
// ErrObjectNotFound until it has synced, ErrObjectDeleted once it
// is deleted. A child that is already here (synced ahead of its
// parent) re-derives regardless.
ParentId string
}
DeriveObjectOpts is the input to ObjectService.Derive.
type DeriveRequest ¶
type DeriveRequest struct {
// Seed is hashed into the derivation. Zero seed = account-root
// derivation (tech space).
Seed []byte
// SpaceType is an app-level tag surfaced as SpaceInfo.SpaceType for
// client-side filtering. It is NOT the on-wire header type (that
// stays SpaceTypeAny and is coordinator-gated) and not stamped into
// the header as the type. Empty defaults to SpaceTypeAny.
SpaceType string
// Name is the initial display name, written on FIRST
// materialization only (a pre-existing row keeps its metadata) and
// NOT hashed into the derivation — the id is stable regardless.
// Propagates into the in-space spaceIndex via the owner-side lazy
// seed; rename later with Space.SetMetadata.
Name string
}
DeriveRequest is the input to Service.Derive.
type Device ¶
type Device struct {
// PeerId is the device's libp2p peer id — the row id. Stable per
// device installation; every device writes only its own row.
PeerId string
// Name is the device's display name (hostname or user-set).
Name string
// OS is the device's operating system (runtime.GOOS vocabulary by
// convention).
OS string
// Version is the device's engine build version.
Version string
// Apps is the set of installed apps keyed by slug (an open set —
// nothing app-specific is hardcoded). Presence = installed; the
// value is a free-form scalar bag (e.g. {"version": "1.2.3"}).
// Nil when the device never registered an app.
Apps map[string]map[string]any
// ActiveClaims holds the device's active claim per app slug,
// written by Service.ClaimActive. Resolve the winner with
// ActiveDevice — never by comparing claims ad hoc.
ActiveClaims map[string]DeviceClaim
}
Device is one row of the account's devices registry. All fields are synced account-wide via the tech space (owner-only ACL — invisible outside the account). Online status deliberately does not live here.
type DeviceClaim ¶
type DeviceClaim struct {
// Seq is max(existing seqs for the slug) + 1 at claim time;
// highest wins.
Seq int64
// At is the claim wall-clock time in unix seconds — tiebreak on
// equal Seq.
At int64
}
DeviceClaim is one active claim: writer-supplied data, NOT a CRDT version id (version ids are peer-locally allocated and not comparable across devices — see SYN-165).
type DeviceUpsert ¶
DeviceUpsert is the input to Service.SetDevice. Only non-empty scalar fields are written; each Apps entry lands per-slug (a nil map value removes the slug — the uninstall signal), so writes touching different fields merge instead of clobbering.
type DiffFilter ¶
DiffFilter narrows a Diff (proposal §7).
type DiffResult ¶
type DiffResult struct {
Base Version
Version Version
Datasets []DatasetDiff
}
DiffResult is the object-level diff between two versions.
type EnsureBundleRequest ¶
type EnsureBundleRequest struct {
// Id is the stable bundle identifier. Required.
Id string
// Name is the display name, written on install ($set — the
// converged value is whichever install wins). Optional.
Name string
// NewRoot creates the bundle's root object and returns its id,
// called only when no winner exists yet. The root must be a
// non-derived object (Objects().Create) — a losing root must be
// deletable, and derived trees are not. Required unless
// DerivedRoot.
//
// Ensure stamps `any.name` (Name, falling back to Id) on the new
// root: the root tree must carry a non-root change to enter the
// head-sync diff, or a losing root could never be resolved from
// another device.
NewRoot func(ctx context.Context) (rootId string, err error)
// DerivedRoot installs the bundle on its CANONICAL DERIVED root —
// derived from the bundle id, so every device computes the same
// root id with zero communication and concurrent installs cannot
// fork. Ensure derives the root itself (NewRoot must be nil) and
// stamps its membership (RootType / RootCollections).
//
// Two consequences, both permanent: the install can never be
// uninstalled (derived trees are not deletable, so a dead-winner
// reinstall is impossible), and a claimed canonical root always
// wins the registry — see BundlesAPI.Ensure. The EXCEPTION, not
// the default: bundles exist so a converged install does not need
// a derived object, and the registry resolves created-root forks.
// Derive only when a fork would be unmergeable — chat-like content,
// above all the 1-1 general chat, where the convergence gate cannot
// work — never for anything a user may remove or for id convenience.
DerivedRoot bool
// RootType is the type of the root Ensure mints — required on a
// derived root that declares nothing (every object has a type),
// written with its name in the root's first change and, on a later
// Ensure, set when the row has no type yet (a type it already has
// is never replaced). Refused next to a declaration (Parts /
// Properties / XKey / Collection): a definition object carries its
// marker in `any.type` and has no type of its own. A caller-minted
// root (NewRoot) gets its type from NewRoot and refuses it here.
RootType string
// RootCollections are added to the root Ensure mints in that same
// change, and on every later Ensure a collection the row lacks is
// added ($addToSet, idempotent), so a request that gains one
// reaches an existing install. NewRoot roots refuse them.
RootCollections []string
// RootProperties seeds the root's property values, keyed owner
// (typeId or collectionId) → propId → value, in that same change
// (their own op), before the install is registered, so a failed
// seed leaves no install to adopt. Install only — an adopt never
// re-seeds, the installer's values sync in. A keyed owner that is
// neither RootType nor the root's own declaration is added to
// RootCollections — a property write to an owner the object does
// not have is rejected. Values are checked to encode before any
// root is minted. The root's own id is a usable key only through
// its declaration: its property ids are derived from (rootId,
// XKey) and known before the install. Roots Ensure mints only.
RootProperties map[string]map[string]any
// Parts declares parts (with their datasets) on the root — derived
// or created — which then defines a type: any.type = "__type__",
// typeId = rootId. Records live on the objects of that type in the
// declared collections (`<rootId>_<key>` for a namespaced dataset,
// the module's canonical collection for a shared one),
// discoverable through Types().Parts(rootId) / Datasets(rootId) and
// Space.Datasets(), writable through Modify/Upsert. The root
// itself hosts them too: a definition object implicitly implements
// itself, which is how a bundle keeps its own records (favourites
// entries, an app's layouts) on its root. Refused with Collection.
// Declared in one change after the registering write, and on adopt
// only when the root's tree is local and carries no declaration
// yet (crash between registering and declaring, a row adopted
// before the root tree synced); a root with any declaration —
// live, or removed through Types().RemovePart — is left alone:
// nothing is patched, added or resurrected by Ensure. Later
// evolution goes through Types().AddPart / AddDataset /
// AddDatasetField / PatchDataset with typeId = rootId. An invalid
// or duplicate draft fails the request with ErrBundleBadRequest
// before any root is minted. With a created strategy, omit NewRoot
// and Ensure mints and stamps the root itself — the only create a
// space with a fenced object lifecycle (the tech space) allows.
// Parts or Properties are required on the tech space.
Parts []PartDraft
// Properties declares property definitions on the root, which then
// defines a type like Parts does — or a collection, with
// Collection set — for a bundle that IS a definition other objects
// use (a wiki collection's `parentId` / `pos`).
// Every draft needs an XKey, unique within the request: the
// property id is DERIVED from (root id, XKey), so two devices
// installing while apart mint one column per handle instead of
// two. Declared in one change after the registering write; on
// adopt only the definitions the root lacks are written — one is
// present when its id exists (live, or removed through
// Types().RemoveProperty: the tombstone keeps the id) or a live
// definition carries its handle under any id, so nothing is
// patched, resurrected or doubled. Later evolution goes through
// Types().AddProperty / PatchProperty / RemoveProperty with typeId
// = rootId; a property added that way gets an ordinary
// change-derived id. Kind, Scope and XKey are validated as
// AddProperty validates them, before any root is minted.
Properties []PropertyDraft
// XKey is the root definition's handle (TypeInfo.XKey /
// CollectionInfo.XKey, stored as `type.xkey` / `collection.xkey`):
// the stable slug a consumer resolves the definition by, and what
// `relation.targetTypes` in other declarations name. An XKey alone
// is a declaration — a MARKER type, or with Collection a marker
// collection objects are filed under as a flag, with no columns.
// Written with the name stamp on install; on adopt it is filled in
// only when the root carries none (an install that predates the
// handle), never changed. Not unique on the SDK side: the consumer
// enforces handle uniqueness.
XKey string
// Layout and Hidden seed the root definition's rendering and
// listing metadata with the name stamp, on install only — adopt
// never patches them. They need a declaration — Parts, Properties
// or XKey (ErrBundleBadRequest otherwise); Layout describes a
// type and is refused with Collection. Hidden is EXPLICIT: a root
// that only hosts its bundle's records should ask for it, since a
// listed type is one a client may set on other objects, granting
// them the bundle's collections; a root that is a definition other
// objects use (a page, a wiki) stays listed.
Layout map[string]any
Hidden bool
// Collection makes the declaration a COLLECTION instead of a type:
// the root carries `__collection__` in `any.type`, its handle and
// flags live under `collection.*`, and Properties are its columns.
// Parts and Layout are refused with it (a collection has neither).
Collection bool
}
EnsureBundleRequest is the input to BundlesAPI.Ensure. Exactly one root strategy is required: NewRoot (a created root) or DerivedRoot (the canonical derived root).
func (EnsureBundleRequest) Declares ¶
func (r EnsureBundleRequest) Declares() bool
Declares reports whether the request makes the root a definition of either kind.
func (EnsureBundleRequest) DeclaresCollection ¶
func (r EnsureBundleRequest) DeclaresCollection() bool
DeclaresCollection reports whether the request makes the root a collection definition — Collection with Properties or an XKey.
func (EnsureBundleRequest) DeclaresType ¶
func (r EnsureBundleRequest) DeclaresType() bool
DeclaresType reports whether the request makes the root a type definition — Parts, Properties, or an XKey alone (a marker type), without Collection.
type EnsureOption ¶
type EnsureOption func(*EnsureOptions)
EnsureOption tunes one Ensure call. Options carry what must never come from a request body: a consumer maps client input onto EnsureBundleRequest and adds options from its own code paths only.
func SystemInstall ¶
func SystemInstall() EnsureOption
SystemInstall marks the call as the consumer's own install — see EnsureOptions.SystemInstall.
type EnsureOptions ¶
type EnsureOptions struct {
// SystemInstall marks the consumer's own catalog install: it lifts
// the reserved-module refusal (handler.Module.Reserved) for this
// call. The reservation exists so only the consumer's installs
// declare such a module.
SystemInstall bool
}
EnsureOptions is the resolved option set.
func ApplyEnsureOptions ¶
func ApplyEnsureOptions(opts ...EnsureOption) EnsureOptions
ApplyEnsureOptions folds opts into an EnsureOptions.
type EventOp ¶
EventOp is one $set or $unset operation inside a SubRecord.Ops slice. Path is the dotted-segment field path; for $set, an empty Path activates the multi-field form (Payload is an object whose keys are dot-separated paths). $inc / $addToSet / $pull / $incGated / delete never reach the wire — those are projected down to $set / $unset against the post-apply value before delivery.
type FieldDiff ¶
FieldDiff is one leaf-level difference; nil Before/After = absent on that side. Peer-local bookkeeping (`_ver`, `_traces`, `_applySeq`, `_addSeq`) never appears here.
type FileInfo ¶
type FileInfo struct {
// FileId identifies the file within its space (the payloads row id).
FileId string
// ObjectId is the object the file is bound to.
ObjectId string
// RootCid is the content address of the encrypted file. Empty for
// inline-tier files (which live inside the row itself).
RootCid string
// Size is the plaintext byte size.
Size int64
// Inline reports the inline tier (no RootCid, no backup needed).
Inline bool
// Durable reports whether the file is backed up on the network (a
// verified custody receipt is recorded on the row). Inline files
// are durable by construction.
Durable bool
// Name is the user-facing file name (member-only; empty without
// the space key).
Name string
// Mime is the content-type hint (member-only).
Mime string
// Cached reports a complete local copy (always true for inline).
Cached bool
// Variant/VariantOf tag alternate representations (member-only;
// empty for originals and keyless readers).
Variant Variant
VariantOf string
}
FileInfo describes one attached file.
type FileListOpts ¶
type FileListOpts struct {
// ObjectId restricts the listing to files bound to one object.
ObjectId string
// Limit caps the result (0 = unlimited). Applied after ObjectId.
Limit int
}
FileListOpts filters List.
type FileReader ¶
type FileReader interface {
io.Reader
io.Seeker
io.Closer
// Size is the plaintext byte length.
Size() int64
}
FileReader is a seekable, sized view of a file's plaintext.
type FileStatus ¶
type FileStatus struct {
FileId string
ObjectId string
State FileSyncState
// Cached reports a complete local copy (inline always true).
Cached bool
// Attempts counts failed background attempts since the last
// success/enqueue; 0 when no work is pending.
Attempts int
// LastErr is the last background-attempt failure ("" when none).
LastErr string
}
FileStatus is the point-in-time durability + availability view of one file.
type FileSyncState ¶
type FileSyncState string
FileSyncState is the durability state of one file.
const ( // FileStateDurable — a verified network receipt is recorded (or the // file is inline and rides the CRDT). FileStateDurable FileSyncState = "durable" // FileStateInFlight — registered, backup not confirmed yet (queued, // uploading, or driven by another device). FileStateInFlight FileSyncState = "inflight" // FileStateLimited — the network refused backup (storage limit). FileStateLimited FileSyncState = "limited" )
type Files ¶
type Files interface {
// Attach ingests r as a file bound to objectId. The whole reader is
// consumed. Attach is local-only and never waits on the network:
// registration is durable in the CRDT immediately, and for
// node-backed files the backup ("durable") phase runs in the
// background. A false FileInfo.Durable means the file is registered
// and locally available but not yet backed up; Status and
// SubscribeStatus observe the flip.
Attach(ctx context.Context, objectId string, r io.Reader, opts AttachOpts) (FileInfo, error)
// Open returns a random-access reader over the file's verified
// plaintext. Content not yet local streams in on demand (every
// fetched block persists, so reads accrete toward a complete local
// copy); a file that is neither local nor durable is not openable
// until the P2P block layer lands. The reader is bound to ctx.
Open(ctx context.Context, fileId string, variant Variant) (FileReader, error)
// Get returns the file's current info (member-only fields like
// Name/Mime are empty for a keyless reader).
Get(ctx context.Context, fileId string) (FileInfo, error)
// Status derives the file's durability state on read: durable
// (verified receipt on the row, inline included), in-flight
// (registered; backup pending or being driven), or limited (the
// network refused backup for storage limit; retried on a slow
// cadence and on Retry).
Status(ctx context.Context, fileId string) (FileStatus, error)
// SubscribeStatus delivers FileStatus events for this space's
// files on LOCAL transitions — attach, backup progress/failure,
// pin completion, manual retries. (Remote flips — another device
// finishing a backup — are visible via Status/Get reads; a synced
// event feed arrives with the SYN-30 files view.) The returned
// function unsubscribes.
SubscribeStatus(cb func(FileStatus)) (unsubscribe func())
// Stats returns this space's aggregate durability counts (UI
// badges: "n files not backed up").
Stats(ctx context.Context) (FileStats, error)
// Pin schedules a full background fetch of the file's content into
// the local store (survives restarts; on-demand reads stay the
// default without it).
Pin(ctx context.Context, fileId string) error
// Retry makes the file's pending background work due immediately —
// a limited file after a quota raise, or any stalled backup. No-op
// with no pending work; re-enqueues the backup when the row is
// unsigned and the bytes are local.
Retry(ctx context.Context, fileId string) error
// Offload drops the file's local bytes, keeping the file itself —
// a later Open transparently refetches from the network. Refused
// unless the file is backed up (never drops the only copy); inline
// files are a no-op. Content shared with other files (dedup) loses
// its local bytes for all of them — each stays refetchable; Pin a
// sibling to keep it hot.
Offload(ctx context.Context, fileId string) error
// Delete removes the file: its payload row is deleted in one synced
// CRDT change (every member sees the file disappear), pending
// background work is cancelled, and the local content ref is
// released so cache GC reclaims the bytes (unreferenced CARs age
// out past the safety-sweep grace period — deletion never races a
// settling sync). Deleting an original also deletes its variant
// rows — they are unresolvable without it; a keyless reader (who
// cannot see VariantOf) deletes only the addressed row. Content
// shared with a surviving row (dedup/BIND) is untouched: each row
// holds its own ref. The network copy is NOT reclaimed here —
// fileprotov2 has no delete RPC yet; the broker's row-driven
// accounting stops counting the rows once the deletion syncs.
// ErrNotFound when no such file exists (Delete is not idempotent
// over the wire — a second call fails like any other read).
Delete(ctx context.Context, fileId string) error
// List returns the space's files as typed infos. Opts.ObjectId
// restricts to one object's files (the fast path — one indexed
// lookup). The unfiltered listing walks every file in the space:
// fine for human-scale spaces, but a consumer tracking a very
// large space should page with Opts.Limit or drive Query/Changes
// instead of re-listing.
List(ctx context.Context, opts FileListOpts) ([]FileInfo, error)
// Query returns the generic query surface (filter / sort /
// subscribe — see space.Query) over the payload rows of ONE
// object's files. Rows expose the cleartext fields (fileId,
// rootCid, size, networkSign, objectId); the member-only meta
// stays sealed — use Get/List for typed access to it.
//
// ErrNotFound until the object's first file is attached (the
// backing dataset materializes with the first Attach) — fall back
// to List/Changes until then.
Query(objectId string) (Query, error)
}
Files is the per-space file surface (docs/files.md). Files always bind to an existing object — there are no standalone file objects; the first Attach lazily creates the object's derived payloads child, and the space-wide files listing is the payloads dataset itself (see PayloadsView).
The storage tiers (inline vs content-addressed + node backup) are invisible here: Attach picks the tier from the content, and Open resolves it through the availability ladder (local cache → network).
type HistoricalView ¶
type HistoricalView interface {
// Version returns the cut handle the view was built at.
Version() Version
// Datasets lists dataset names this view can serve.
Datasets() []string
// Record returns one record (nil if absent at this version).
// Deleted records surface as tombstones (`_deletedAt` set).
Record(ctx context.Context, dataset, recordId string) (*anyenc.Value, error)
// Records returns all live records of a dataset at this version.
Records(ctx context.Context, dataset string) ([]*anyenc.Value, error)
Close() error
}
HistoricalView is a read-only projection of an object at a version. Backed by an in-memory scratch store; Close releases it. Not safe for use after Close.
type HistoryAPI ¶
type HistoryAPI interface {
// ListChanges lists an object's history, newest first (descending
// causal order: parents never appear after children; timestamps
// are author-supplied and display-only). Pagination via the opaque
// cursor returned in ChangeList. The first call on an object whose
// index is stale triggers a lazy backfill.
ListChanges(ctx context.Context, objectId string, f HistoryFilter, limit int, cursor string) (ChangeList, error)
// ViewAt materializes the object as of version — object or (via
// filter in the returned view's Records calls) dataset scope. The
// view holds an in-memory projection of the SYNCED scope only:
// local/account-scope values have no history in the object's DAG
// and are excluded rather than misleadingly shown current. The
// caller must Close.
ViewAt(ctx context.Context, objectId string, version Version) (HistoricalView, error)
// RecordAt reconstructs one record as of version — the chat-scale
// fast path (filtered replay ∩ causal past). Returns nil when the
// record does not exist at that cut; a deleted record surfaces as
// its tombstone (check the `_deletedAt` field).
RecordAt(ctx context.Context, objectId, dataset, recordId string, version Version) (*anyenc.Value, error)
// Diff computes the structural difference between two versions.
// base == "" means version's parents — the per-change effect diff
// ("what did this change actually land", ops gated out by newer
// concurrent writes excluded). Scope narrows via the filter.
Diff(ctx context.Context, objectId string, base, version Version, f DiffFilter) (DiffResult, error)
}
HistoryAPI is the per-space version-history surface (proposal §7).
type HistoryFilter ¶
type HistoryFilter struct {
Dataset string // only this dataset
RecordId string // only changes touching this record (requires Dataset)
TraceId string // only changes carrying this trace id
Author string // identity filter
Coalesce *CoalesceOpts // group keystroke-grained changes; nil = raw
}
HistoryFilter narrows ListChanges (proposal §7).
type IdRule ¶
Behavioral dataset-schema vocabulary, aliased from the handler package (one vocabulary for compiled-in and runtime declarations).
type IdentitiesAPI ¶
type IdentitiesAPI interface {
// List returns every known identity.
List(ctx context.Context) ([]IdentityInfo, error)
// Get returns a single identity, ok=false when never encountered.
Get(ctx context.Context, identity string) (info IdentityInfo, ok bool, err error)
// Subscribe streams add/update/remove batches for the directory.
// The returned func cancels the subscription.
Subscribe(cb func(IdentityListEvent)) (cancel func())
}
IdentitiesAPI is the account-global directory of every account identity this account has encountered — across spaces, 1-1s, and inbox invites. It is a device-local, persistent cache: profiles are resolved from identityRepo and the spaceIds set tracks where each identity was seen. (The decryption keys behind it sync across the account's devices; the profiles themselves are re-derived per device.)
type IdentityInfo ¶
type IdentityInfo struct {
// Identity is the account address (the row key).
Identity string
// Name / Description / IconCID are the last profile resolved from
// identityRepo; empty until resolved (or if we lack the key).
Name string
Description string
IconCID string
// SpaceIds is the set of spaces where we've currently seen this
// identity (pruned when we leave/offload a space).
SpaceIds []string
}
IdentityInfo is a point-in-time view of one directory entry.
type IdentityListEvent ¶
type IdentityListEvent struct {
Added []IdentityInfo
Updated []IdentityInfo
Removed []string
}
IdentityListEvent is a batch of directory changes, mirroring SpaceListEvent.
type Indexer ¶
type Indexer interface {
// OnSpaceCreated runs after a new space is successfully created or
// joined. The hook is expected to add a record to the space index
// in the tech space. Errors are logged but do not abort the
// creation — the space exists either way.
OnSpaceCreated(ctx context.Context, spaceId string, meta SpaceInfo) error
// OnSpaceDeleted runs after local deletion. The hook flips the
// space index record to Status = StatusDeleted; the record is
// never physically removed (see docs/tech-space.md).
OnSpaceDeleted(ctx context.Context, spaceId string) error
// OnSpaceMetadataUpdated mirrors the converged state of the
// per-space `spaceIndex` derived object into the tech-space row.
// Fired by the per-space watcher every time the spaceIndex
// object's property record applies (locally or pushed from a
// peer). Idempotent overwrite of name/description/icon — `type`
// stays pinned by the tech-space handler. See
// docs/space-index-proposal.md for the convergence story.
OnSpaceMetadataUpdated(ctx context.Context, spaceId string, meta SpaceInfo) error
}
Indexer is the seam through which the tech space plugs into space lifecycle events. internal/techspace implements it; sdk.Open wires it into the space.Service. space/ itself never imports techspace (that would reverse the layering).
type Invite ¶
type Invite struct {
SpaceId string
InviteKey crypto.PrivKey
// Kind distinguishes a member invite (RequestToJoin proof key) from
// a guest invite (the shared read-only guest identity itself). Zero
// value is member — pre-Kind invites decode as members.
Kind InviteKind
}
Invite is the share-friendly handle a space owner produces and gives to someone they want to let in. Opaque to callers; encoded as a single base58 token so it survives copy/paste through chats and links without escaping.
Internally an invite carries (spaceId, invitePrivKey). The invite private key is the one any-sync's BuildInvite minted on the owner side; the joiner uses it as proof against the matching public-key invite record on the ACL.
SDK-defined format — independent of anytype-heart's encrypted-blob invite. There is no compatibility shim.
func DecodeInvite ¶
DecodeInvite parses a base58 token produced by EncodeInvite back into its components. Returns ErrInvalidInvite (wrapped with detail) for any malformed input — callers can use errors.Is.
type InviteInfo ¶
type InviteInfo struct {
RecordId string
// Permission applies only to AnyoneCanJoin invites — for the
// RequestToJoin path it is set at accept time. Always
// PermissionNone here in v1.
Permission Permission
// Key is the invite private key when THIS account minted the
// invite: recovered from the account's synced issued-key custody
// (the ACL record carries only the public key), so it is present
// on every device of the minting account and nil everywhere else —
// other members', even admins', devices never held it. Also nil
// for invites minted before custody shipped (re-mint once to make
// them recoverable) and for custody gone stale (invite replaced /
// revoked elsewhere). Non-nil Key re-encodes to the original share
// token via EncodeInvite(Invite{SpaceId, InviteKey: Key}).
Key crypto.PrivKey
}
InviteInfo is one active invite. RecordId is what ACL.RevokeInvite expects.
type InviteKind ¶
type InviteKind byte
InviteKind is the invite flavor carried by an encoded invite token.
const ( // InviteKindMember is a RequestToJoin invite: InviteKey proves the // holder may request to join; membership lands via owner approval. InviteKindMember InviteKind = iota // InviteKindGuest is a public-access invite: InviteKey IS the // shared guest identity's private key. Holders open the space // read-only via Service.JoinGuest — no ACL write, no approval. InviteKindGuest )
type Iterator ¶
Iterator streams query results. Usage:
it, _ := q.Iter(ctx)
defer it.Close()
for it.Next() {
doc, err := it.Doc()
...
}
type JoinRequest ¶
type JoinRequest struct {
// Invite is the invite string produced by ACL.CreateInvite on the
// host side. The SDK parses it internally; opaque to the caller.
Invite string
// Metadata is the joining account's metadata that will be attached
// to the ACL join record (display name, icon, etc.).
Metadata AccountMetadata
}
JoinRequest is the input to Service.Join.
type JoinRequestInfo ¶
type JoinRequestInfo struct {
// RecordId is the ACL record id — pass it to ACL.AcceptRequest.
RecordId string
// Identity is the joiner's account id.
Identity string
// Name / Description / IconCID are decoded from the metadata blob
// the joiner attached at request time.
Name string
Description string
IconCID string
}
JoinRequestInfo is one pending join request, kept as a convenience projection over Members.List() for admin UIs.
type Member ¶
type Member struct {
Identity string
Permission Permission
Status MemberStatus
Name string
Description string
IconCID string
// RequestRecordId is set only when Status == MemberStatusJoining.
// Pass it to ACL.AcceptRequest / DeclineRequest.
RequestRecordId string
}
Member is one row in the members view. Status partitions the rows:
- MemberStatusActive — confirmed member with read/write access per Permission.
- MemberStatusJoining — has an outstanding join request awaiting owner approval. RequestRecordId is set; Permission is None.
- MemberStatusRemoved — was a member, removed by the owner. Kept as a tombstone so UIs can render "Alice left this space".
- other transient states mirror any-sync's AclStatus.
Name / Description / IconCID are decoded from the metadata blob the joiner attached at request time. identityRepo-backed enrichment (resolving names/icons published by the account itself elsewhere) is a follow-up.
type MemberAdd ¶
type MemberAdd struct {
Identity string
Permission Permission
Metadata AccountMetadata
}
MemberAdd is one entry in a batch AddAccounts call. Metadata is optional and gets attached to the join record (same shape as the metadata a joiner attaches via Service.Join).
type MemberEvent ¶
type MemberEvent struct {
Kind MemberEventKind
Member Member
Previous *Member
}
MemberEvent is one delivery on a Subscribe firehose. Kind tells the subscriber what changed; Member carries the post-event state; Previous carries the pre-event state (nil for Added, set otherwise).
type MemberEventKind ¶
type MemberEventKind uint8
MemberEventKind is the discriminator on MemberEvent.
const ( // MemberEventAdded — the identity wasn't in the previous snapshot. // Fires for new active members and new pending join requests. MemberEventAdded MemberEventKind = iota + 1 // MemberEventChanged — same identity, different fields. Fires for // permission upgrades/demotions, status flips (e.g. accept moves // joining → active), or metadata updates. MemberEventChanged // MemberEventRemoved — identity gone from the snapshot. Fires for // dropped pending requests (decline / cancel) and on full-member // removal where the tombstone is also dropped (rare — usually a // Changed event with Status=Removed lands instead). MemberEventRemoved )
type MemberStatus ¶
type MemberStatus uint8
MemberStatus is the membership lifecycle stage. Mirrors any-sync's list.AclStatus, plus MemberStatusJoining for pending join requests (which any-sync tracks in a separate map but the SDK surfaces as part of the same members view — see docs/space.md § "Members as a collection").
const ( MemberStatusUnknown MemberStatus = iota MemberStatusJoining MemberStatusActive MemberStatusRemoved MemberStatusDeclined MemberStatusRemoving MemberStatusCanceled )
func (MemberStatus) String ¶
func (s MemberStatus) String() string
String returns the canonical wire label for the status — "unknown" / "joining" / "active" / "removed" / "declined" / "removing" / "canceled". Also the on-disk representation in the members collection (see MembersAPI.Query). Unknown values stringify as "unknown".
type MembersAPI ¶
type MembersAPI interface {
// List returns every account currently visible in the ACL: active
// members, removed-member tombstones (Status=Removed), and pending
// join requests (Status=Joining). Order is not stable across calls.
List(ctx context.Context) ([]Member, error)
// Get returns a single member entry by identity, or ErrNotFound.
// Pending join requests are reachable here too.
Get(ctx context.Context, identity string) (Member, error)
// Me returns the caller's own member entry. Convenient shortcut
// for surfacing the caller's role in the UI.
Me(ctx context.Context) (Member, error)
// JoinRequests returns the subset of List() with Status=Joining —
// kept as a convenience for admin UIs that approve / decline.
JoinRequests(ctx context.Context) ([]JoinRequestInfo, error)
// Invites returns the active invites for this space, with their
// record ids (use these as inputs to ACL.RevokeInvite).
Invites(ctx context.Context) ([]InviteInfo, error)
// Subscribe registers a firehose listener. cb runs synchronously
// from the watcher goroutine — keep work small or hand off to your
// own goroutine. The returned cancel function detaches the
// subscriber; it's safe to call multiple times.
//
// Events cover: a new member appearing (active or pending),
// permission/status flips, and members disappearing (pending
// requests dropped on accept/decline/cancel; full members keep
// a Status=Removed tombstone via a Changed event).
Subscribe(cb func(MemberEvent)) (cancel func())
// Query returns a chainable query builder over the materialized
// members system collection. Supports the same Filter / Sort /
// Limit / Offset shape as Space.Query / Space.QueryObjects.
//
// The collection is kept in sync with the underlying ACL by the
// per-space watcher (poll interval ~250 ms). Reads here may lag
// in-memory state (List / Get / Me) by at most one tick; for
// strictly current state use those direct methods.
//
// Schema fields (all strings unless noted):
// id — identity (== Member.Identity)
// permission — "owner" / "admin" / "writer" / "reader" / "guest" / "none"
// status — "active" / "joining" / "removed" / "removing" /
// "declined" / "canceled" / "unknown"
// name — decoded display name
// description — decoded description
// icon — IconCID
// requestId — present only for status=joining (recordId for accept)
Query() Query
}
MembersAPI is the read-side facade over a space's ACL state. Reads are point-in-time snapshots derived from the locally replicated ACL list — fast, in-memory.
Live updates are delivered via Subscribe — a firehose of MemberEvent messages covering add / remove / change for both confirmed members and pending join requests. The implementation polls the underlying AclList head id at a small interval (~250 ms); subscribers see new state within one tick of arrival.
type ModifyBatch ¶
type ModifyBatch struct {
ObjectId string
Dataset string
Records []RecordModify
// TraceIds are opaque caller-supplied correlation tokens that
// travel through any-sync in a dedicated field so the whole space
// can be queried by trace. Empty slice means no trace.
TraceIds []string
// Scope selects the write route for the whole batch. A write call
// is single-scope — routes commit in different version domains and
// there is no cross-route rollback (same rule as PropertiesAPI.Set).
// Zero value = ScopeSynced: the object's own DAG change, synced to
// every member. The default, and the only route ModifyMany and
// Delete support.
//
// ScopeLocal is the device-local materialization route: no DAG
// change, never syncs, VersionId minted by the local lexid
// allocator; the write flows through Query/Subscribe like any
// apply. Every op must target a field the dataset schema declares
// ScopeLocal — ops on fields of any other scope are refused by the
// apply layer's scope enforcement and surface in
// ModifyResult.Rejections, like handler rejections on the synced
// route. Records must already exist: explicit ids, no Upsert —
// local fields annotate synced records, they don't create them
// (a strict-mode miss surfaces as an ErrStrictSkipAbsent
// rejection). TraceIds are rejected (they ride the any-sync
// change). The shared `objects` dataset is rejected too: its
// per-property scopes are enforced by the writer, so local
// property values go through PropertiesAPI.Set.
//
// ScopeAccount and ScopeDerived are rejected: derived is
// handler-only, and the account transport for dataset records is
// not wired yet — the account mirror handles objects rows only
// (docs/scoped-properties-proposal.md § Account transport).
Scope Scope
}
ModifyBatch is the caller-facing write batch: one or more record changes in a single dataset of a single object. Applied atomically and returns one VersionId for the whole batch.
type ModifyResult ¶
type ModifyResult struct {
VersionId VersionId
ChangeId string
RecordIds []string
Rejections []OpRejection
}
ModifyResult bundles the identifiers Space.Modify and Space.Delete return for any successful batch.
- VersionId is the peer-local lexid stamped on the batch's records. Compare with CompareVersion against other VersionIds this peer has observed.
- ChangeId is the any-sync DAG change id — content-addressable, stable across peers. Use it for tracing / cross-peer correlation.
- RecordIds is the per-record id list aligned to the input RecordModify slice. For records the caller submitted with an empty Id, the resolved value is `base58(xxh3-64(ChangeId))` (with `:<index>` suffix for the second-and-later empty ids in a batch). This is the propId / shortId convention; callers creating types or properties read it from RecordIds[0].
- Rejections lists per-op handler rejections — ops that the change carries but the handler refused (kind mismatch, terminal status, immutable field, unknown property…). The change still committed with a fresh VersionId, but those ops did not land. HTTP layers can surface this as a partial success or a hard error per their policy.
type Mutability ¶
type Mutability = handler.Mutability
Behavioral dataset-schema vocabulary, aliased from the handler package (one vocabulary for compiled-in and runtime declarations).
type ObjectChange ¶
ObjectChange identifies an object that applied a change, paired with the per-space applySeq watermark that change advanced it to. It is the unit of both the live feed (Subscribe) and the catch-up query (ChangedSince).
ApplySeq is the SDK's per-space, strictly local, monotonic apply counter. Unlike any-sync's AddSeq (the DAG delivery counter, which only DAG-borne changes have), applySeq advances on EVERY apply that mutates this space's records: synced changes, the tech-space account mirror's applies, and device-local writes — so a consumer cursoring on it never misses a non-DAG mutation. Treat it as an opaque ordering key within this space on this device: compare and persist it as a cursor, never ship it to another peer or treat it as a network clock. Records carry the matching per-record stamp as `_applySeq`.
Continuity with the pre-applySeq feed (keyed on AddSeq): legacy per-object watermarks are backfilled applySeq := addSeq once per space, and the allocator seeds past the historical maximum, so a cursor persisted in AddSeq units stays valid on the applySeq axis.
Deleted is true when the object was purged (object deletion): the entry carries a fresh ApplySeq strictly greater than the object's last content change, so it evicts in the same ordered stream as edits. Consuming Deleted is MANDATORY for eviction — a deleted object never re-appears as a content change.
type ObjectDebug ¶
type ObjectDebug struct {
ObjectId string
// Sync state — from the per-space syncstatus.Tracker. Pending
// is the set of heads the tracker is waiting for a
// responsible-node HeadsApply on; empty means converged with
// the last sender we trust.
SyncState SyncState
Pending []string
LastSyncAt time.Time
// Tree structure — read from any-sync's ObjectTree.
//
// HeadsCount == len(Heads), exposed as a separate field so a
// caller checking "is this object diverged right now" doesn't
// need to len() the slice.
//
// BranchCount counts every branch that ever existed in the
// DAG: merged-in lineages (one per extra parent on a merge
// change) plus still-open ones (HeadsCount - 1). A linear
// tree with one head returns 0.
//
// Snapshots is the number of changes carrying IsSnapshot in
// the tree. Costs a full IterateRoot walk — O(TreeLen).
Heads []string
HeadsCount int
BranchCount int
TreeLen int
Snapshots int
// LatestVersionId is the lexid-max OrderId across Heads. Empty
// string when the tree has no heads (root-only or transient
// states during cold restore). The value is local to this
// peer's any-sync — VersionIds don't match across peers.
LatestVersionId string
// MaxAddSeq is the controller's delivery-order watermark — the
// highest AddSeq seen by the apply path. Used internally for
// cold-restore replay; surfaced here as a sanity check that
// the controller is keeping up with the tree.
MaxAddSeq uint64
}
ObjectDebug is a point-in-time snapshot of one object's tree-structure + sync-state + local CRDT state. Returned by DebugAPI.Object.
The tree-structure fields (Heads, HeadsCount, BranchCount, TreeLen, Snapshots, LatestVersionId) are read under the object tree's mutex so they're jointly consistent. Local writes are blocked for the duration of the read — on a million-change tree the walk can take a noticeable pause, which is the caller's price for an atomic snapshot.
type ObjectReadState ¶
type ObjectReadState struct {
ObjectId string
// StateSeq is the cursor axis — same per-space monotonic domain
// as ObjectChange.ApplySeq. Persist the last seen value and pass
// it back to ChangedSince.
StateSeq uint64
}
ObjectReadState is one element of the read-state feed: an object whose read state changed (new unread, a mark, a cross-device merge, a delete clearing entries), at the StateSeq that change advanced it to. The feed carries no per-change detail — the consumer re-pulls UnreadSnapshot for the object and diffs against what it holds.
type ObjectService ¶
type ObjectService interface {
// Get returns the object's row from the per-space objects
// collection (membership and property values). An object whose tree
// is present locally but that never wrote a row returns {id} only;
// ErrNotFound when the id is unknown here, ErrObjectDeleted when
// the object's tree is deleted — both read from the space's
// any-sync storage.
Get(ctx context.Context, objectId string) (*anyenc.Value, error)
// Create a fresh object. Returns the any-sync-assigned objectId.
// Type and Collections seed the object's membership at birth;
// InitialProperties seeds the per-space properties record.
Create(ctx context.Context, opts CreateObjectOpts) (objectId string, err error)
// Derive a deterministic object from a seed. Re-runs idempotently;
// second call with the same seed returns the same objectId.
Derive(ctx context.Context, opts DeriveObjectOpts) (objectId string, err error)
// Delete marks the object as deleted (any-sync settings tree) and
// wipes local any-store state. See docs/object.md §"Deletion".
Delete(ctx context.Context, objectId string) error
}
ObjectService is the object lifecycle surface on a space, plus the single-object row read. Record reads, writes and subscriptions happen at the space level keyed by objectId.
type ObjectSyncStatus ¶
ObjectSyncStatus is the per-object sync state. Returned by SyncStatusAPI.Object and delivered on SubscribeObject events.
Unknown objectIds return State == SyncStateUnknown. ObjectId always matches the request (or the tracked id for an event).
type Op ¶
type Op struct {
Type OpType
Path string
// Value is the operand — scalar, slice, or map[string]any
// depending on Type. A value means what its JSON form means:
// Extended-JSON wrappers are typed values, the same spelling a
// Query.Filter literal uses, and {"$date": "<RFC 3339>"} is the
// one with a property kind (datetime); $binary, $oid and $vector
// decode too but fit only undeclared fields. A Go time.Time is a
// datetime and []byte is binary, at any depth of map[string]any /
// []any. A *fastjson.Value is decoded under the same rule; a
// *anyenc.Value is taken verbatim. Every value written into a
// dataset record follows this rule: UpsertRecord.Fields,
// PropertiesAPI.Set, CreateObjectOpts.InitialProperties,
// PropertyPatch.Set, DatasetDefPatch.Set. Tech-space settings and
// device app bags are scalar-only (Service.SetSettings,
// DeviceUpsert.Apps).
Value any
}
Op is one mongo-style modifier.
Path is a dotted field path ("a.b.c"). For $set and $unset, an empty Path activates the multi-field form: Value is interpreted as an object whose keys are paths applied in parallel under one VersionId.
type OpRejection ¶
type OpRejection struct {
RecordIndex int
RecordId string
OpIndex int
Reason string
ReasonErr error
}
OpRejection describes one op the handler refused to apply. The containing change still committed; this op's effect did not.
- RecordIndex / RecordId identify the affected record in the input batch.
- OpIndex is the index into RecordModify.Ops that was rejected; -1 means the whole record was rejected (BeforeCreate / BeforeDelete).
- Reason is the human-readable error string the handler returned. Programmatic discrimination uses the SDK's typed errors via errors.Is on the underlying error — exposed via ReasonErr.
type OpType ¶
type OpType string
OpType is the set of supported modifiers for v1. No insert op — record creation is opt-in via RecordModify.Upsert.
type P2PState ¶
type P2PState uint8
P2PState is the direct-peer sync state of a space: whether this device is connected to LAN or global peers that share it.
const ( P2PStateUnknown P2PState = iota // P2PStateNotPossible — p2p is disabled in config, the device has // no usable network interface, or local discovery is switched off // with no LAN peer known and no global layer to find one. P2PStateNotPossible // P2PStateNotConnected — a p2p layer is on but no direct peer // sharing this space is connected. P2PStateNotConnected // P2PStateConnected — at least one direct peer (LAN or global) // sharing this space has a live connection. P2PStateConnected // P2PStateRestricted — the OS denies local-network access (e.g. // iOS Local Network permission). P2PStateRestricted )
type PartDef ¶
type PartDef struct {
Id string // part record id, immutable
Key string
Name string
Icon string
Pos string
Hidden bool
UI map[string]any
Uses []string
Datasets []DatasetDef
}
PartDef is the compiled view of one part.
type PartDraft ¶
type PartDraft struct {
// Key is the part's slug, unique within the type — pinned.
Key string
// Name / Icon / Pos are the display slice; clients sort parts by
// Pos. Hidden parts are not shown by default but stay revealable.
Name string
Icon string
Pos string
Hidden bool
// UI is the widget descriptor — a slug plus an opaque config in the
// x-format shape ({type, config}). Written whole; nil = the first
// dataset's module default.
UI map[string]any
// Uses names other datasets OF THIS TYPE the part renders without
// owning them (keys).
Uses []string
// Datasets are the part's initial dataset declarations.
Datasets []DatasetDraft
}
PartDraft is the input to TypesAPI.AddPart: the part's key and display slice plus its initial datasets.
type PayloadRow ¶
type PayloadRow struct {
// FileId identifies the row — derived from the creating change,
// deterministic and never reused.
FileId string
// RootCid is the UnixFS root of the encrypted file. Empty for
// inline-tier rows (bytes ride inside the sealed blob).
RootCid string
// Size is the plaintext byte size (cleartext hint; quota uses the
// node's own measurement).
Size int64
// NetworkSign is the node's durable-custody receipt. Empty until
// the file is durable; always empty for inline rows.
NetworkSign string
// Author is the account that registered the file (derived from the
// creating change's signer).
Author string
// ObjectId is the object the file is bound to (the cleartext parent
// reference).
ObjectId string
// Sealed reports whether the row's member-only secrets remain
// closed to this identity. True for a keyless reader (the broker
// view — not an error); false when the local account holds the
// space key, even though the secrets are still not exposed here.
Sealed bool
}
PayloadRow is the cleartext (broker) view of one payloads record — everything a node needs for refcount / GC / quota / durability. The sealed member-only secrets (wrapped file key, name, sha256, mime, inline bytes) are not exposed on this surface.
type PayloadsView ¶
type PayloadsView interface {
// ListObjects returns the objectIds of the materialized payloads
// objects in this space (one per file owner, derived lazily on the
// owner's first registration). Classified by the tree root's
// changeType — signed and content-addressed, so the set can't be
// spoofed. Trees known only as heads-only stubs (selective sync) or
// deleted trees are excluded.
ListObjects(ctx context.Context) ([]string, error)
// ListRows returns every live row of one payloads object. The
// object must be materialized locally (an id from ListObjects);
// rows come back in collection order.
ListRows(ctx context.Context, payloadsObjectId string) ([]PayloadRow, error)
}
PayloadsView is the read-only surface over the space's file `payloads` objects — the node-readable per-file index of the files subsystem. It exposes exactly the cleartext row fields, so a keyless embedder (the filenode-v2 broker: headless + selective sync) can enumerate and index every registered file without holding any space key; the member-only secrets stay sealed and are never surfaced here.
Obtained from Space.Payloads.
type PeerSyncStats ¶
PeerSyncStats is the latest snapshot of one outbound diff round against a peer.
New + Changed are the counts handed to the SDK's TreeSyncer after deletionState filtering — i.e. trees we'll actually pull or push, not the raw pre-filter diff. LastSyncAt is the completion time of the round; LastErr is the SyncAll error string if any, empty on success.
"Deleted" is intentionally absent in v1 — any-sync routes removedIds through the deletion path before our TreeSyncer sees them, so we can't attribute a clean per-peer count from our side without an upstream hook.
type Permission ¶
type Permission uint8
Permission mirrors any-sync's ACL permission ladder.
const ( PermissionNone Permission = iota PermissionReader PermissionGuest PermissionWriter PermissionAdmin PermissionOwner )
func ParsePermission ¶
func ParsePermission(s string) Permission
ParsePermission maps a canonical wire label back onto the enum. Unknown labels (including "") parse as PermissionNone — absent and no-access are the same answer for every caller.
func ParsePermissionStrict ¶
func ParsePermissionStrict(s string) (Permission, bool)
ParsePermissionStrict is ParsePermission with an explicit ok: false for any label that is not a canonical permission ("none" included as valid). For callers validating external input, where an unknown label must be an error rather than silently no-access.
func (Permission) String ¶
func (p Permission) String() string
String returns the canonical wire label for the permission — "none" / "reader" / "guest" / "writer" / "admin" / "owner". The inverse of ParsePermission; unknown values stringify as "none".
type PermissionChange ¶
type PermissionChange struct {
Identity string
Permission Permission
}
PermissionChange is one entry in a batch ChangePermissions call.
type ProjectionOpts ¶
type ProjectionOpts struct {
// IncludeDeleted returns record-level tombstone rows (id, _deletedAt,
// _ver, _traces, _addSeq; content wiped) instead of skipping them —
// e.g. a deleted record inside a still-live object's dataset. Honored
// by the find path (Iter / All / One / Count); Snapshot/Subscribe keep
// skipping tombstones (the windowed live view is unchanged).
//
// Note: a deleted OBJECT is NOT a tombstone. Its whole local
// projection is purged (any-sync's head storage is the durable,
// cross-device delete record), so IncludeDeleted surfaces nothing for
// it on QueryObjects. Observe object deletions via
// QueryObjects().Subscribe (a Removed{RemoveDeleted} event), or by
// reconciling against any-sync's deleted-tree set.
IncludeDeleted bool
}
ProjectionOpts controls result visibility knobs. Reserved fields (`_ver`, `_traces`, `_deletedAt`) are always present — `_ver` is caller-facing by contract (clients reconcile optimistic state per field against it; see the CRDT spec §3).
type PropertiesAPI ¶
type PropertiesAPI interface {
Get(ctx context.Context, objectId string) (*anyenc.Value, error)
// Set merges the patch into the object's property record. ownerId
// is the type or collection whose properties the patch names; keys
// are propIds and all must resolve to the SAME declared scope.
// Unknown keys, kind mismatches, mixed-scope patches, and an owner
// the object does not have are rejected before anything is
// written.
Set(ctx context.Context, objectId, ownerId string, patch map[string]any) (ModifyResult, error)
// SetType replaces the object's type. The previous type's values
// and dataset records become orphan data, read-tolerant; its
// datasets refuse further writes. A known collection id is refused
// (ErrWrongSlot).
SetType(ctx context.Context, objectId, typeId string) (ModifyResult, error)
// AttachCollection adds the object to a collection ($addToSet —
// idempotent). A known type id is refused (ErrWrongSlot).
AttachCollection(ctx context.Context, objectId, collectionId string) (ModifyResult, error)
// DetachCollection removes the object from a collection ($pull).
// Values in that namespace become orphan data, read-tolerant.
DetachCollection(ctx context.Context, objectId, collectionId string) (ModifyResult, error)
}
PropertiesAPI reads and writes the per-object property record in the space's shared `objects` collection (one row per object).
Every property lives in exactly ONE scope, declared on its definition (see Scope / PropertyDraft.Scope). Values sit at their normal `{typeId}.{propId}` paths regardless of scope — there are no per-scope record fields and no read-time merging. What the scope decides is the WRITE ROUTE and the version domain:
- synced — the object's own CRDT change; syncs to everyone with access; VersionId is the object tree's orderId.
- account — a carrier record in the account's private tech space, mirrored into this row on each of the account's devices; VersionId is the tech tree's orderId. Invisible to other members.
- local — written straight into this device's row, never synced; VersionId is a locally-minted lexid.
Read path: Get returns the row verbatim. In a shared space, account/local-scoped values reflect THIS account/device — queries and filters over them select per-account / per-device result sets.
Write path: one auto-routing Set. The SDK resolves each patch key's declared scope and issues the write on that scope's route. A patch whose keys span MORE THAN ONE scope is rejected (routes commit independently and cannot be rolled back together — callers issue one call per scope instead). One call = one route = one VersionId domain in the returned ModifyResult.
SetType writes the object's one type (`any.type`, a scalar LWW register — every object has exactly one, so there is no unset); AttachCollection / DetachCollection edit its collections set (`any.collections`). Membership is structural and shared, so all three always route through the object's own CRDT (synced).
type PropertyDef ¶
type PropertyDef struct {
Id string // base58(xxh3-64(changeId)), immutable
Name string // display label, CRDT-mutable
Description string // CRDT-mutable
// XKey is an optional stable caller-side key (e.g. for generated
// client code mapping). Not unique, not enforced — metadata only.
XKey string
// Meta is an opaque consumer-controlled flag map. The SDK stores
// and returns it verbatim and never interprets it — e.g. the `any`
// server's search indexer reads meta["index"] = "<scope>" to mark
// a property as full-text/vector indexable. Nil when unset.
// Deliberately not schema-bearing: CRDT-mutable via PatchProperty
// (meta.<k> paths).
Meta map[string]string
Kind PropertyKind // first-write-wins; immutable
Items *PropertyDef // for arrays
Properties []PropertyDef // for objects
Required []string // for objects — CRDT-mutable additions
// Scope is the property's write/sync class (synced / account /
// local; derived on built-ins). First-write-wins like Kind —
// immutable for the propId's life. Definitions written before
// scopes existed read back as ScopeSynced.
Scope Scope
// XFormat is the property's opaque descriptor — semantic slug,
// icon, ordering key, option set, relation targets, per-format
// config — as the consumer wrote it. The SDK stores it verbatim,
// never interprets it, and lets every path under it mutate
// (PatchProperty); a whole-bag write must be an object. Nil for
// definitions that carry none. Decoded from the record with plain
// Go values: nested objects as map[string]any, arrays as []any,
// numbers as float64, instants as time.Time, binaries as []byte,
// object ids as hex strings, float vectors as []float64. Every
// view is a fresh copy — the caller's to mutate.
XFormat map[string]any
}
PropertyDef is the live shape of one property definition. All fields reflect the current (post-merge) state; renames and other CRDT-mutable changes are visible via List / Properties refresh.
type PropertyDefsAPI ¶
type PropertyDefsAPI interface {
// Properties returns the current property definitions of the
// owner. Built-ins (`any`, `type`, `collection`, `spaceIndex`) and
// registered definitions answer their static tables.
Properties(ctx context.Context, ownerId string) ([]PropertyDef, error)
// AddProperty mints a new property on the owner. The returned
// propId is base58(xxh3-64(changeId)) — immutable for the life of
// the property. Registered owners refuse (ErrTypeRegistered).
AddProperty(ctx context.Context, ownerId string, draft PropertyDraft) (propId string, err error)
// RemoveProperty drops a property definition. Existing value
// records are NOT cleaned up; subsequent writes touching that
// property are dropped op-by-op via the unknown-property rule.
RemoveProperty(ctx context.Context, ownerId, propId string) error
// PatchProperty applies a generic per-path patch to a property
// definition. Set assigns values at dotted paths; Unset removes
// them. Both merge per-path through the record's CRDT, so
// concurrent edits to different paths converge.
//
// Mutable paths: name, description, x-key, meta.<k>, x-format and
// every path under it (any JSON value — the SDK stores the
// descriptor opaquely; the consumer owns its vocabulary and its
// leaf-only patch rule). Pinned paths (key, kind, scope, items,
// properties) are rejected — define a new property to change them.
PatchProperty(ctx context.Context, ownerId, propId string, patch PropertyPatch) error
}
PropertyDefsAPI is the property-definition surface shared by types and collections: one implementation, addressed by the owning definition's id (a type id or a collection id) through either Space.Types() or Space.Collections(). Definitions live on the owner object; values live on the objects row under `<ownerId>.<propId>`.
type PropertyDraft ¶
type PropertyDraft struct {
Name string
Description string
XKey string
Meta map[string]string // opaque consumer flags — see PropertyDef.Meta
Kind PropertyKind
Items *PropertyDraft
Properties []PropertyDraft
Required []string
// Scope is the property's write/sync class. Zero value means
// ScopeSynced. ScopeDerived is reserved for built-ins and rejected.
// Pinned by the first write — to change a property's scope, define
// a new property (which mints a new propId).
Scope Scope
// XFormat optionally declares the descriptor at creation, written
// as one whole object (see PropertyDef.XFormat). Values convert as
// Op.Value describes; nothing inside is validated — the consumer
// owns the vocabulary.
XFormat map[string]any
}
PropertyDraft is the input to TypesAPI.AddProperty. Kind, Items, Properties, and Scope are locked by the first write; the rest remain mutable.
type PropertyKind ¶
type PropertyKind uint8
PropertyKind mirrors the JSON-Schema-subset types supported on property definitions. See docs/types-properties-proposal.md.
The zero value is intentionally unnamed — every property has a concrete kind, so a zero PropertyKind is a caller-side error (e.g. a PropertyDraft with Kind unset). The SDK validates this on the write path.
const ( PropertyKindString PropertyKind = iota + 1 PropertyKindNumber PropertyKindBoolean PropertyKindNull PropertyKindArray PropertyKindObject // PropertyKindDatetime is an instant, stored as any-store's native // TypeDateTime (unix millis, orderable, index-keyable, `{"$date": …}` // in JSON). The kind the `date` / `datetime` formats imply. PropertyKindDatetime )
type PropertyPatch ¶
PropertyPatch is a generic per-path patch to a property definition, the input to PatchProperty.
Set maps a dotted field path to its new value (values convert as Op.Value describes and are not otherwise validated). Unset lists dotted field paths to remove (subtree removals are allowed, e.g. "x-format.options.<key>" to delete a whole option). A path present in neither is left unchanged. At least one entry across Set / Unset is required.
Pinned paths are rejected before any write (see PatchProperty).
type PubSubAPI ¶
type PubSubAPI interface {
// Publish sends payload on topic. Returns fast after local
// validation, signing and encryption — network delivery is
// asynchronous and unacknowledged. Local subscribers with a
// matching pattern receive the message synchronously (Self=true).
Publish(ctx context.Context, topic string, payload []byte) error
// Subscribe registers cb for messages matching pattern and pushes
// the interest to the space's peers. cb runs on a single shared
// dispatch goroutine and MUST NOT block — slow handlers drop
// messages for every subscriber. Do not call Publish synchronously
// from cb while holding locks cb needs. The returned cancel is
// idempotent; cb may fire once more after cancel returns.
Subscribe(pattern string, cb func(PubSubMessage)) (cancel func(), err error)
}
PubSubAPI is ephemeral, space-scoped pub/sub: fire-and-forget, at-most-once, no persistence, no replay. Messages are encrypted with the space read key, signed by the sender, and fanned out to the space's members via the responsible sync node and any directly connected LAN peers. Offline members simply miss messages — payloads must be idempotent or last-write-wins.
Topics are `/`-separated (e.g. "chat/room1"). Subscribe patterns may use NATS-style wildcards: `*` matches exactly one segment, a trailing `>` matches one or more. The `acc/…/<accountId>` namespace is self-owned — only that account can publish under it (enforced at publisher, relay and receiver), which makes it spoof-proof for presence-style topics.
Guest-mode (public access) spaces are not supported: the transport signs as the account identity, which a guest space's ACL does not contain — Publish and Subscribe fail fast there.
The handle is always non-nil; per-call state is checked at use time.
type PubSubMessage ¶
type PubSubMessage struct {
// SpaceId the message was published in.
SpaceId string
// Topic the publisher targeted (concrete, no wildcards).
Topic string
// SenderIdentity is the sender's account address (same form as
// Account().Id() / Member.Identity).
SenderIdentity string
// Self is true when the message is the loopback delivery of this
// account's own publish (any device of the account).
Self bool
// Payload is the decrypted application payload.
Payload []byte
}
PubSubMessage is one verified, decrypted pub/sub message handed to a Subscribe callback. SenderIdentity is proven by the message's signature (checked before dispatch) — the authoritative sender account, not anything self-declared inside Payload.
type PushAPI ¶
type PushAPI interface {
// SetToken registers this device's platform push token (APNs / FCM)
// with the server. Call again on token rotation; RevokeToken on
// logout.
SetToken(ctx context.Context, platform PushPlatform, token string) error
// RevokeToken removes this device's push token — the device stops
// receiving notifications for the account.
RevokeToken(ctx context.Context) error
// RegisterSpace registers the space's derived push key with the
// server, enabling Notify/Subscribe for its topics. Requires read
// access to the space's ACL (members only). Idempotent — the server
// treats a re-registration as success.
RegisterSpace(ctx context.Context, spaceId string) error
// RemoveSpace unregisters the space's push key — its topics and
// subscriptions are dropped server-side.
RemoveSpace(ctx context.Context, spaceId string) error
// SubscribeAll replaces the account's ENTIRE topic set across all
// spaces (server semantics: full replace, not a merge). Callers own
// the reconcile: compute the complete desired set — every space,
// every topic — and submit it in one call; anything absent is
// unsubscribed.
SubscribeAll(ctx context.Context, subs []PushSpaceTopics) error
// Subscriptions returns the account's current topic set as the
// server holds it — raw {base58 spaceKey, topic} rows, without
// signatures. Use it to diff local desired state against the server
// before a SubscribeAll.
Subscriptions(ctx context.Context) ([]PushSubscription, error)
// Notify publishes an encrypted notification to the given topic
// strings within spaceId. The payload (cleartext JSON by
// convention) is encrypted with the space's derived key and signed
// by this account; only space members can decrypt. groupId is the
// server-side collapse key (e.g. a chat id) — an opaque grouping
// hint, empty for none.
Notify(ctx context.Context, spaceId string, topics []string, payload []byte, groupId string) error
// NotifySilent wakes the account's OWN other devices with a
// data-only push (no user-visible message) — e.g. after a read-state
// change, so their badges refresh. It targets only the caller's
// own-identity topic within spaceId; the server drops any other
// topic on the silent path.
NotifySilent(ctx context.Context, spaceId string, groupId string) error
}
PushAPI is the client surface of the push-notification node — an out-of-band peer (config.Push) that fans mobile push notifications out to the account's registered device tokens by topic.
The server is zero-knowledge about space content: topics are signed with a per-space key derived from the ACL (only members hold it) and notification payloads are encrypted with a key derived from the space's current read key — the server relays ciphertext. All methods are account-scoped (the server identifies the caller by the secure channel's account key) and return ErrPushNotConfigured when no push node is configured.
type PushKeys ¶
type PushKeys struct {
// SpaceKey is the base64 (std) of the protobuf-marshalled ed25519
// private key that identifies the space on the push server
// (pushapi Topic.SpaceKey is its public half). Derived from the
// ACL's first metadata key — fixed for the space's life.
SpaceKey string
// EncKey is the base64 (std) of the raw AES payload key derived
// from the CURRENT ACL read key.
EncKey string
// EncKeyId is hex(sha256(raw EncKey bytes)) — the value stamped
// into pushapi.Message.KeyId, i.e. the cache key a receiver looks
// up on an incoming push.
EncKeyId string
}
PushKeys is the per-space key material a push RECEIVER needs, byte-compatible with anytype-heart's spacePushNotificationKey / spacePushNotificationEncryptionKey space-view details.
EncKey rotates with the ACL read key. Clients must treat their cache as append-only per space ({EncKeyId → EncKey}): a payload encrypted before a rotation still arrives carrying the old KeyId, and a KeyId that was never cached (fresh install, rotation missed while offline) means "render a generic notification".
EncKey is a one-way SLIP-21 derivation from the read key — holding it decrypts push payloads only, never space data.
type PushPlatform ¶
type PushPlatform string
PushPlatform selects the mobile push transport a device token belongs to. String-typed so config/wire layers can pass it through verbatim; the SDK maps it onto the push server's proto enum.
const ( PushPlatformIOS PushPlatform = "ios" PushPlatformAndroid PushPlatform = "android" )
type PushSpaceTopics ¶
PushSpaceTopics is one space's slice of a SubscribeAll request: the topic strings this device wants notifications for within SpaceId. Topic strings are an app-level vocabulary (e.g. anytype-heart uses "chats", per-chat sha256 ids, and the account identity for mentions); the SDK signs them with the space's push key but does not interpret them.
type PushSubscription ¶
PushSubscription is one raw row from PushAPI.Subscriptions: the base58-encoded space push PUBLIC key (the server's space identifier — not a spaceId; the mapping is client-side via the derived key) and the topic string. Signatures are not returned by the server.
type Query ¶
type Query interface {
// Filter applies a query condition. Anything query.ParseCondition
// accepts works — an already-built query.Filter, a JSON-shaped
// string ("{\"any.name\":\"Casablanca\"}"), or a map literal with
// mongo-style operators ($eq, $gt, $in, $and, $or, …). The
// argument is parsed eagerly; a malformed filter surfaces on the
// first terminal call (Iter / All / One / Count), not silently.
// A map literal is converted like a record value (see Op.Value):
// a time.Time or []byte in it is typed, and a {"$date": …}
// literal is an instant, so a datetime field is filtered by either
// spelling.
Filter(filter any) Query
// Sort orders results by the given keys. Anything query.ParseSort
// accepts works — string keys ("-_ver.id" for descending) or
// already-built query.Sort values. Parsed eagerly; errors surface
// on the terminal call.
Sort(sorts ...any) Query
// Limit caps returned records. Zero means unlimited.
Limit(n int) Query
// Offset skips the first n records. Use cautiously for large result
// sets — cursor-based pagination is a future add.
Offset(n int) Query
// Projection controls which reserved fields appear in returned
// records. See ProjectionOpts.
Projection(opts ProjectionOpts) Query
// Iter opens a streaming iterator. Caller must Close when done.
Iter(ctx context.Context) (Iterator, error)
// All materializes every match into a slice. Convenience wrapper
// around Iter; avoid for large result sets.
All(ctx context.Context) ([]*anyenc.Value, error)
// One returns the first match or (nil, ErrNotFound).
One(ctx context.Context) (*anyenc.Value, error)
// Count returns the match count without returning documents.
Count(ctx context.Context) (int, error)
// Snapshot is the typed terminal that returns an initial result set
// plus an optional one-shot total count, without subscribing for
// live updates. Same result shape as Subscribe — callers that only
// need a point-in-time view can use this instead of chaining
// All + Count separately.
Snapshot(ctx context.Context, opts QueryOpts) (*QueryResult, error)
// Subscribe returns the initial snapshot AND a live QuerySubscription
// whose mailbox carries incremental updates (added / updated /
// removed records within the windowed view). The window tracks the
// chained filter / sort / limit; offset applies to the initial
// snapshot only.
//
// Sub closes with ErrSubscriptionOverflow when its mailbox fills,
// or ErrSubscriptionDrifted when too many in-window records leave
// without replacements. Both signal "resubscribe to recover".
Subscribe(ctx context.Context, opts QueryOpts) (*QueryResult, error)
}
Query is the caller-facing query builder, returned by Space.Query. Chainable — each option returns a new Query (the underlying builder may be mutable but the public contract is immutable-looking).
type QueryOpts ¶
type QueryOpts struct {
// IncludeTotal asks for a one-shot count of filter-matching records
// (independent of limit/offset), returned in QueryResult.Total, and
// derives QueryResult.HasNext from it. No live total events are
// emitted by Subscribe; callers who need a refreshed count call
// Snapshot again. When false, Total is -1 and HasNext is false.
IncludeTotal bool
// MailboxCapacity bounds the per-subscription event queue. Default
// 256, minimum 16. On overflow the subscription closes; Wait
// returns mb.ErrClosed and Err() returns ErrSubscriptionOverflow.
// Subscribe-only.
MailboxCapacity int
// DriftBudgetPercent: when more than this fraction of Limit
// records leave the held window without replacements, the
// subscription closes with ErrSubscriptionDrifted. Default 30.
// Ignored when Limit == 0. Subscribe-only.
DriftBudgetPercent int
}
QueryOpts is the option block for Snapshot and Subscribe. The same struct serves both terminals; subscription-only fields are documented and ignored by Snapshot.
type QueryResult ¶
type QueryResult struct {
Initial []*anyenc.Value
Total int // -1 unless QueryOpts.IncludeTotal=true
HasNext bool // true when more matches exist past this page (offset+len(Initial) < Total); always false when Total is unknown
Sub QuerySubscription // nil for Snapshot
}
QueryResult is what Snapshot and Subscribe return. Sub is nil for Snapshot, non-nil for Subscribe. Initial is always the materialised point-in-time view bounded by the chained limit/offset.
type QuerySubscription ¶
type QuerySubscription interface {
// Events returns the underlying mb/v3 mailbox. Use Wait for
// batched delivery or WaitOne for single events.
Events() *mb.MB[SubscriptionEvent]
// Err returns the close reason after the mailbox closes. nil
// while the subscription is live or after a user-initiated Close.
Err() error
// Close releases the subscription. Idempotent.
Close() error
}
QuerySubscription is the live handle returned by Query.Subscribe. It mirrors the shape of Subscription but carries SubscriptionEvent (added/updated/removed within a windowed view) instead of the raw per-change Event.
The mailbox closes for one of three reasons:
- Caller invoked Close: Err() returns nil.
- Mailbox overflowed: Err() returns ErrSubscriptionOverflow.
- Held window drifted past its budget: Err() returns ErrSubscriptionDrifted.
In both error cases the client should resubscribe to recover.
type ReadStateAPI ¶
type ReadStateAPI interface {
// Subscribe fires after a committed read-state change for an
// object. cb runs synchronously on the notifying path — keep it
// small or hand off.
Subscribe(cb func(objectId string, stateSeq uint64)) (cancel func())
// ChangedSince returns objects whose read state advanced past
// since, ascending by StateSeq, capped at limit (0 = no cap) —
// one element per object. Re-pull UnreadSnapshot per dirty object
// and diff against your held set.
ChangedSince(ctx context.Context, since uint64, limit int) ([]ObjectReadState, error)
// UnreadSnapshot returns the object's full current unread set
// (ascending by VersionId) and the object's current stateSeq —
// the per-object pull after a ChangedSince hit, and the cold
// start / resync entry point.
UnreadSnapshot(ctx context.Context, objectId string) ([]UnreadChange, uint64, error)
// UnreadCounts returns the object's per-tag unread counters.
UnreadCounts(ctx context.Context, objectId string) (map[string]int, error)
// MarkRead covers the given changes and their causal ancestry,
// then publishes the account's read frontier to the account's
// other devices.
MarkRead(ctx context.Context, objectId string, changeIds []string) error
// MarkReadUpTo covers every unread change with VersionId <= upTo —
// "this and everything before" in this device's display order.
// "" reads everything. Backs Read(message) / ReadAll() sugar.
MarkReadUpTo(ctx context.Context, objectId string, upTo crdt.VersionId) error
// Generation is the same per-space rebuild epoch as
// ChangeIndexAPI.Generation — cursors reset when it changes.
Generation(ctx context.Context) (string, error)
}
ReadStateAPI tracks read/unread changes for datasets that opted in via handler.Dataset.ReadTracking. Read state is private to the account (synced across its devices through the tech space, never visible to other space members) and marking is forward-only: a change, once read, never becomes unread again.
Same consumption contract as ChangeIndexAPI: Subscribe is a best-effort liveness ping; ChangedSince from a persisted cursor is the durable catch-up returning dirty OBJECTS (state, not a log — it never grows and never prunes); UnreadSnapshot is both the per-object pull after a dirty mark and the full resync after a Generation change.
type RecordDiff ¶
RecordDiff is one record's difference between two versions.
type RecordModify ¶
RecordModify groups ops applied to one record id.
Id: when empty, the CRDT layer derives one from the change's ChangeId (base58(xxh3-64(ChangeId))); subsequent empty-id records in the same batch get a ":<index>" suffix. Empty id requires Upsert=true.
Upsert: false (default) = strict update-if-exists — modifies on an absent record are silent no-ops. true = create-or-update; the record is auto-created if absent. Tombstones stay sticky in both modes.
type RemoveReason ¶
type RemoveReason uint8
RemoveReason classifies why a record left the visible window, carried per id on SubscriptionEvent.Removed. RemoveDeleted means the object is gone from the database; the other two mean it left your result set but still exists, so a Snapshot/Query.One would still return it.
const ( // RemoveDeleted: the record was tombstoned; it no longer exists in // the database. A Query.One for this id now returns ErrNotFound. RemoveDeleted RemoveReason = iota // RemoveFilteredOut: an update changed a field so the query's filter // no longer matches the record. The record still exists. RemoveFilteredOut // RemoveDisplaced: a higher-priority arrival (or the record's own // sort-key change) pushed it past the Limit boundary. The record // still matches the filter; it just sits outside the visible window. RemoveDisplaced )
func (RemoveReason) String ¶
func (r RemoveReason) String() string
String renders the reason for logging and test output.
type RemovedRecord ¶
type RemovedRecord struct {
Id string
Reason RemoveReason
}
RemovedRecord is one id that left the visible window, tagged with the cause. Symmetric with SubRecord on Added/Updated, minus the doc/ops — a removed record carries no post-apply payload.
type Scope ¶
Scope is the unified write/sync class shared by property definitions and dataset schema fields — how a value is written, which version domain stamps its `_ver` entries, and how far it syncs. A property lives in exactly ONE scope, declared at creation and pinned for the propId's life (like Kind); there is no per-value override stack.
Aliased from the handler package so the whole SDK uses one type and one label vocabulary (synced / derived / account / local).
func ParseScope ¶
ParseScope parses a scope's wire label ("synced" / "derived" / "local" / "account") — the inverse of Scope.String. Returns (0, false) on an unknown label.
type SearchFields ¶
type SearchFields = handler.SearchFields
Behavioral dataset-schema vocabulary, aliased from the handler package (one vocabulary for compiled-in and runtime declarations).
type Service ¶
type Service interface {
// Create a new regular space owned by the authenticated account.
Create(ctx context.Context, req CreateRequest) (Space, error)
// Join a space via an invite. Depending on the invite key mode the
// space is either immediately active or pending approval. The
// pending join is account-wide: the row reads StatusJoining on every
// device of the account, none materializes the space, and the
// device that observes the acceptance loads it and flips the row
// active for the rest. A row left by an earlier join that ended
// without membership (owner declined, or withdrawn via CancelJoin —
// surfaced as StatusDeleted) is revived: the request is posted again
// and the row returns to StatusJoining. A synced tombstone (Delete)
// is sticky and refused with ErrSpaceDeleted before anything reaches
// the network.
Join(ctx context.Context, req JoinRequest) (Space, error)
// CancelJoin withdraws this account's pending join request for
// spaceId (a row in StatusJoining) — from any of the account's
// devices, not only the one that requested. Nothing local is
// materialized: the request record lives on the space's ACL chain,
// which the nodes serve directly, so the cancel is posted through
// the joining client the way Join posted the request. On success the
// row flips to StatusDeleted account-wide — the same end state an
// owner decline leaves — and a later Join with a valid invite
// revives it. ErrSpaceUnknown for an id with no row;
// ErrJoinNotPending when the row is not joining or the owner
// accepted the request first (see the sentinel for what happens to
// the row then). A request already gone from the chain with no
// membership behind it — withdrawn or declined elsewhere before the
// marker synced — is settled here: the row is marked ended, nil. A
// chain that cannot be read (offline) is reported as the transport
// error and the row is left as it is.
CancelJoin(ctx context.Context, spaceId string) error
// JoinGuest adds a space via a guest invite (InviteKindGuest): the
// invite carries the shared read-only guest identity, so there is no
// join request and no owner approval — the space is pulled and opened
// signing as that identity. One bounded synchronous load attempt is
// made; if the content isn't pullable yet the row is recorded
// durably, (nil, ErrGuestJoinPending) is returned, and loading
// finishes in the background (resumed across restarts). The space is
// read-only for life: synced writes fail with ErrReadOnlySpace and
// Info reports OwnRole = PermissionGuest. Remove it with Delete
// (local drop — a guest cannot write to the ACL).
JoinGuest(ctx context.Context, invite string) (Space, error)
// Derive a deterministic space from the account keys. Used for
// the tech space (never returned here) and future derived spaces.
// Derived spaces are permanent — Delete refuses them with
// ErrIsDerivedSpace (see the sentinel for why).
Derive(ctx context.Context, req DeriveRequest) (Space, error)
// DeriveId returns the deterministic spaceId for a DeriveRequest
// without creating or loading the space. Same id as Derive(...).Id()
// for the same request. Lets a consumer recompute a known derived
// space's id (from its own seed) to recognize or filter it
// client-side.
DeriveId(ctx context.Context, req DeriveRequest) (string, error)
// OneToOne reaches out to — or explicitly accepts / un-declines — the
// derived 1-1 space shared with otherIdentity. Materializes and
// activates it locally (implicit self-approval). Same id regardless of
// key order — both peers land on the same space. Idempotent; overrides
// a prior local decline.
OneToOne(ctx context.Context, otherIdentity string) (Space, error)
// AcceptOneToOne approves an incoming pending 1-1 by space id (as
// surfaced in List with Status == StatusOneToOnePending): materializes
// and activates it. The peer identity is read off the row, so the
// caller needn't re-derive it. Equivalent to OneToOne(peer).
AcceptOneToOne(ctx context.Context, spaceId string) (Space, error)
// DeclineOneToOne rejects an incoming 1-1. Writes a synced sticky
// marker so the request is suppressed on all the account's devices and
// never auto-resurfaces; a later explicit OneToOne(peer) overrides it.
DeclineOneToOne(ctx context.Context, spaceId string) error
// RegisterIncoming records an incoming 1-1 request learned out-of-band
// (no coordinator) as a pending row for the user to approve, without
// materializing storage. displayHint is an optional name/icon snapshot
// for the UI. No-op if a row for the derived space already exists.
RegisterIncoming(ctx context.Context, peerIdentity string, displayHint AccountMetadata) error
// AcceptInvite approves a direct-add invite (a space surfaced in List
// with Status == StatusInvitePending or StatusInviteDeclined — the
// account is already an ACL member; accept is a local materialization
// gate). Flips the synced status to active (all the account's devices
// converge) and loads the space. When the content isn't pullable yet
// it returns ErrInviteAcceptPending and loading continues durably in
// the background (crash/restart-safe); poll Get or Subscribe for the
// flip. Idempotent; overrides a prior decline.
AcceptInvite(ctx context.Context, spaceId string) (Space, error)
// DeclineInvite rejects a direct-add invite. Writes a synced sticky
// marker so the request is suppressed on all the account's devices; a
// later AcceptInvite overrides it. No ACL write happens — the account
// remains a member on the space's ACL. Nothing was materialized, so
// nothing is removed.
DeclineInvite(ctx context.Context, spaceId string) error
// Get returns an already-joined space by id. Fails if the space is
// unknown locally, with ErrSpaceNotAccepted for a known row whose
// acceptance is still pending (joining / incoming 1-1 / direct-add
// invite) — those must never be materialized by a read — and with
// ErrSpaceDeleted for a deleted row. A 1-1 accepted or initiated on
// another of the account's devices (synced remote=active) is adopted
// transparently: Get materializes it locally, no per-device
// re-accept needed.
//
// The account's own tech space id (SDK.TechSpaceId) returns a
// restricted handle: reads, dataset declarations on bundle roots,
// derived-only Bundles() and generic record writes work; the
// system datasets are read-only through it and every lifecycle
// surface (objects, types, members, ACL, files, history, …)
// returns ErrUnsupported. It never appears in List / Subscribe.
Get(ctx context.Context, spaceId string) (Space, error)
// Track registers a foreign spaceId in the local space index without
// joining it, so a later Get can open it — any-sync bootstraps the
// space from its responsible nodes when local storage is missing.
// The caller does not become a member and holds no keys: synced
// content stays sealed. Idempotent; a no-op when the id is already
// indexed (including own/joined spaces — Track never downgrades a
// membership row).
//
// The broker path (headless + selective sync): Track the spaceId,
// Get it, read the payloads index via Space.Payloads.
Track(ctx context.Context, spaceId string) error
// Evict closes a space without deleting anything: per-space watchers
// stop, the in-memory store and the any-sync space are released. All
// disk state stays — a later Get reopens the space from local
// storage. Idempotent; evicting a space that isn't open is a no-op.
//
// This is close-on-demand for embedders that hold many spaces (the
// filenode-v2 broker); Delete is the destructive sibling.
Evict(ctx context.Context, spaceId string) error
// List returns a point-in-time snapshot of all known spaces.
// Mirror of the tech space's space index.
List(ctx context.Context) ([]SpaceInfo, error)
// SyncSpaceList forces an immediate head-sync round on the tech
// space so spaces added or removed on other devices land in the
// local index, instead of waiting for the periodic timer. Call it
// before List to converge the space list on demand. Blocks until
// the round completes.
SyncSpaceList(ctx context.Context) error
// WaitListSynced blocks until the tech space (the account's space
// list) has completed a clean head-sync round with no trees parked
// for retry — the restore-path gate before deciding "does space X
// exist on this account" (creation-vs-restore split): after it
// returns, List reflects the responsible node's converged view.
// Retries rounds until ctx expires; unlike SyncSpaceList (one round,
// error verbatim) a transiently offline node keeps it waiting rather
// than failing.
WaitListSynced(ctx context.Context) error
// Delete tears down a space locally. For regular spaces this also
// flags the space as deleted on the network; for 1-1 spaces it is
// local-only (the space is always re-derivable). The record stays
// in List with Status = StatusDeleted. Seed-derived spaces are
// refused with ErrIsDerivedSpace (permanent), the tech space with
// ErrIsTechSpace, and an id with no index row with ErrSpaceUnknown.
// A row in StatusJoining is withdrawn (CancelJoin) rather than
// tombstoned: the request leaves the chain and the row reads
// StatusDeleted but stays re-joinable — a tombstone would leave the
// request pending and the space unjoinable for this account forever.
// If the owner accepted meanwhile, the delete proceeds as for any
// member's space.
Delete(ctx context.Context, spaceId string) error
// SetSettings patches the account-private per-space client settings
// — a free-form object on the space's tech-space row, synced across
// the account's devices (owner-only ACL: other space members never
// see it). Each set entry lands as its own $set at settings.<key>
// and each unset key as a $unset, all in one change — so devices
// editing DIFFERENT keys converge without clobbering each other;
// the same key is per-key LWW.
//
// Keys are the caller's vocabulary: non-empty, dot-free (one level
// under `settings` in v1). Values are scalars — string, bool, or
// any numeric type (stored as float64, JSON semantics). At least
// one set or unset entry is required; a key may not appear in both.
// The spaceId must be known to the account (any row in List,
// deleted included — a tombstone's settings stay editable).
//
// Read back via List / Get → SpaceInfo.Settings, or live via the
// raw spaces-dataset query (Query(SpaceIndexObjectId(), "spaces"))
// where the subtree appears under the row's `settings` field.
SetSettings(ctx context.Context, spaceId string, set map[string]any, unset []string) error
// SetDevice upserts THIS device's row in the account's devices
// registry — a system dataset in the tech space, one row per device,
// synced account-wide (SYN-165). The row id is always the local peer
// id (SDK.PeerId()), never caller-supplied. Only non-empty fields are
// written; each Apps entry lands per-slug (nil value removes the
// slug), so writes touching different fields merge. At least one
// field must be non-empty (ErrDeviceEmptyUpsert). ErrDevicePruned
// when this device's row was deleted — the sticky tombstone
// absorbs the write and the id can never re-register.
//
// Read back via ListDevices, or generically via
// Query(SpaceIndexObjectId(), "devices").
SetDevice(ctx context.Context, up DeviceUpsert) error
// ClaimActive marks THIS device as the active instance of app: it
// writes an activeClaims.<app> = {seq, at} claim on the own row
// (seq = max existing + 1). Conflict-resolution semantics live in
// the reader — resolve the winner with ActiveDevice, never by
// comparing claims ad hoc. There is no un-claim: only a higher
// claim from another device or a row deletion moves the winner.
// ErrDevicePruned when this device's row was deleted (see
// SetDevice).
ClaimActive(ctx context.Context, app string) error
// DeleteDevice prunes peerId's row — the "device doesn't exist"
// signal that moves the active election away from it. The tombstone
// is sticky: the peer id can never re-register (a pruned device
// that comes back stays unlisted until it re-derives its peer
// keys). ErrDeviceUnknown when the row doesn't exist;
// ErrDeviceSelfDelete for the local device's own row (self-pruning
// would permanently lock this installation out of the registry —
// prune it from another device).
DeleteDevice(ctx context.Context, peerId string) error
// ListDevices returns a point-in-time snapshot of the devices
// registry (pruned rows excluded). Feed it to ActiveDevice to
// resolve the active instance of an app. Unavailability (tech
// space not open yet) is an error, never an empty snapshot — an
// election consumer must not mistake a closed service for an
// empty registry.
ListDevices(ctx context.Context) ([]Device, error)
// Subscribe delivers space-list changes (added / updated / removed).
// Returns a cancel function.
Subscribe(cb func(SpaceListEvent)) (cancel func())
// SpaceIndexObjectId returns the id of the tech-space index object —
// the handle for generic Query/Subscribe over the system datasets
// (spaces, profile, devices). Future system objects expose their own ids.
SpaceIndexObjectId() string
// Query builds a generic read query over a system object's dataset
// (e.g. SpaceIndexObjectId() + "spaces"), with the same chainable
// Filter / Sort / Limit / Snapshot / Subscribe surface as
// Space.Query. The bespoke List / Subscribe methods are convenience
// wrappers over this.
Query(objectId, dataset string) Query
// Datasets returns the JSON-Schema description of the tech-space
// system datasets (spaces, profile, devices) — field names, value shapes, and
// per-field class (synced / derived / local) via `x-scope`. For
// discovery, mirroring Space.Datasets.
Datasets() []DatasetSchema
// Status returns a snapshot of one space's rolled-up sync state.
// Cheap; safe to call on every render tick. Spaces unknown to
// the SDK return SpaceSyncStatus{SpaceId: spaceId,
// State: SyncStateUnknown}.
Status(spaceId string) SpaceSyncStatus
// SubscribeStatus delivers SpaceSyncStatus events whenever any
// known space's rollup transitions. Account-wide — one cb sees
// every space. cb runs synchronously on the dispatcher
// goroutine; keep work small or hand off.
SubscribeStatus(cb func(SpaceSyncStatus)) (cancel func())
}
Service is the space-level entrypoint exposed by the top-level SDK. It owns lifecycle (Create / Join / Derive / Delete) and the space list; individual space operations live on Space.
type SetMetadataRequest ¶
SetMetadataRequest is the input to Space.SetMetadata. Pointer semantics: nil = leave-unchanged; non-nil empty string = set-empty.
type Space ¶
type Space interface {
// Id returns the space identifier (any-sync CID-based).
Id() string
// Info returns a point-in-time snapshot of space metadata.
Info() SpaceInfo
// Objects is the object lifecycle API (Create / Derive / Delete).
Objects() ObjectService
// ACL exposes the full any-sync ACL feature set: invite, accept,
// decline, change permissions, ownership transfer, self-remove.
ACL() ACL
// Members returns the members-collection view.
Members() MembersAPI
// Types manages type objects (and, through PropertyDefsAPI, the
// property definitions of types and collections alike).
Types() TypesAPI
// Collections manages collection objects — definitions with
// properties only, which objects are filed under.
Collections() CollectionsAPI
// Properties reads computed property values and performs
// base/account/device scope writes.
Properties() PropertiesAPI
// SyncStatus exposes per-space/object/peer status.
SyncStatus() SyncStatusAPI
// Changes exposes the change-index surface: a live feed and a
// "changed since cursor N" query over the objects in this space,
// for consumer-side incremental indexers (full-text / vector
// search). See ChangeIndexAPI.
Changes() ChangeIndexAPI
// History exposes version history: list an object's changes (who
// changed what, when), view objects/records at past versions, and
// diff versions. See HistoryAPI and
// docs/version-history-proposal.md.
History() HistoryAPI
// Payloads is the read-only view over the space's file payloads
// index — the cleartext row fields only, readable without any
// space key. See PayloadsView.
Payloads() PayloadsView
// Files is the file surface: attach content to objects, with the
// storage tiers (inline / content-addressed + node backup) hidden.
// See Files.
Files() Files
// PubSub is ephemeral space-scoped pub/sub — fire-and-forget,
// at-most-once messages between the space's online members, never
// persisted. See PubSubAPI.
PubSub() PubSubAPI
// ReadState tracks read/unread changes for datasets registered
// with handler.Dataset.ReadTracking. Methods return
// ErrReadTrackingDisabled when nothing in the space opted in.
ReadState() ReadStateAPI
// Debug returns the diagnostic surface for this space. See
// DebugAPI — not a stable interface, intended for tooling and
// inspection.
Debug() DebugAPI
// Query builds a read query against (objectId, dataset). Used for
// per-object datasets that exist on the object's own controller —
// e.g. a type object's `properties` (definitions) dataset.
Query(objectId, dataset string) Query
// QueryObjects builds a read query against the per-space `objects`
// collection — one row per regular object in the space, holding
// computed property values. Use this for cross-object queries
// like "find every Movie with Title containing X".
QueryObjects() Query
// Aggregate builds a MongoDB-style aggregation pipeline against
// (objectId, dataset) — the aggregation sibling of Query. The
// pipeline is accepted in the same forms Query.Filter takes a
// condition: a JSON string, *fastjson.Value, *anyenc.Value,
// marshaled-anyenc []byte, or a Go value converted like a record
// value (see Op.Value). A Go map has no key order, so a stage whose
// key order matters ($sort over several keys) is given as JSON
// text or *fastjson.Value. Snapshot-only. See Agg.
Aggregate(objectId, dataset string, pipeline any) Agg
// AggregateObjects builds an aggregation pipeline against the
// per-space `objects` collection — the aggregation sibling of
// QueryObjects. See Agg.
AggregateObjects(pipeline any) Agg
// Datasets returns the JSON-Schema description of every dataset in
// this space — field names, value shapes, and per-field class
// (synced / derived / local) via the `x-scope` keyword. For
// discovery; per-type object property schemas are also available
// through Types().
Datasets() []DatasetSchema
// Modify applies a write batch. Returns the VersionId, ChangeId,
// and resolved per-record ids (auto-derived ids surface here for
// callers who submitted records with empty Id — propId / shortId
// convention).
Modify(ctx context.Context, batch ModifyBatch) (ModifyResult, error)
// ModifyMany applies multiple write batches in one logical
// submission, with pre-validation as the atomicity boundary:
// every batch is structurally validated up-front, and if ANY
// validation fails NONE are written to any-sync. Otherwise
// each batch produces its own DAG change in input order.
//
// All batches MUST target the same ObjectId — cross-object
// atomicity is not supported (each object's tree signs its own
// changes). Datasets may differ across batches; this is the
// supported way to land properties + a user dataset together
// in a single client submission.
//
// Per-op handler rejections at apply time still surface as
// ModifyResult.Rejections, same as Modify — the apply layer is
// per-op by design (convergent across peers regardless of
// arrival order).
//
// The returned slice is aligned to input order. Validation
// failure returns an empty slice and the joined error.
ModifyMany(ctx context.Context, batches []ModifyBatch) ([]ModifyResult, error)
// Delete produces sticky tombstones for the listed record ids.
// Returned RecordIds mirror the input order.
Delete(ctx context.Context, batch DeleteBatch) (ModifyResult, error)
// Upsert is the generic schema-driven batch ingest: upsert by
// record id, diff only declared-mutable fields against stored
// values, skip identical records, one change per page. Requires an
// id:user dataset (the caller id is the idempotency key). See
// UpsertBatch for per-record semantics.
Upsert(ctx context.Context, batch UpsertBatch) (UpsertResult, error)
// SetMetadata mutates this space's display metadata (name,
// description, icon) by writing to the per-space `spaceIndex`
// derived object. The write is CRDT-replicated to every member;
// each device's indexer hook mirrors the converged state into its
// own tech-space row.
//
// Pointer-to-string semantics: a nil pointer means "leave
// unchanged"; a non-nil pointer to an empty string means "set to
// empty". This lets callers patch a single field without
// clobbering the others.
//
// SpaceType is intentionally not settable — it's pinned by the
// initial Create write on the spaceIndex object.
//
// v1 has no caller-side permission gate; non-writers are rejected
// downstream by the ACL at apply time on peers.
SetMetadata(ctx context.Context, req SetMetadataRequest) error
// SetP2PAdvertise switches per-space p2p advertising
// (SpaceInfo.P2PAdvertise). On republishes this device's global p2p
// record into the space right away; off stops the heartbeat — the
// account's other devices follow within a day, and rows already
// written age out on the other members' devices over 30 days. Synced
// account-wide; a read-only member can flip it, but readers never
// write rows, so for them it changes nothing.
SetP2PAdvertise(ctx context.Context, on bool) error
// SpaceIndexObjectId returns the deterministic id of the in-space
// `spaceIndex` derived object. Stable across peers and across
// SDK reboots — same id on every member's device. Useful for
// wrappers that want to attach a Query.Subscribe stream on the
// spaceIndex's `objects` dataset for live UI updates.
SpaceIndexObjectId() string
// Bundles is the typed surface over the per-space installed-bundles
// registry on the spaceIndex object. See BundlesAPI.
Bundles() BundlesAPI
// WaitIndexSynced blocks until the local view of the space's index
// is trustworthy — the gate the restore path takes before reading
// the bundles registry ("wait for the space index, then see what is
// set up"). Two exits: the seeded metadata row is already projected
// locally (immediate, no network — offline-first), or a clean
// head-sync round completes with the sync-status rollup at Synced,
// which also covers spaces that never seed metadata (1-1 / nameless
// derived spaces) — there an absent index after convergence is the
// valid answer "nothing set up yet", not a wait-forever. While
// waiting it forces head-sync rounds; bounded only by ctx (an
// offline device with no local index keeps waiting).
WaitIndexSynced(ctx context.Context) error
// SyncHeads forces an immediate head-sync (diff) round on this
// space against its responsible nodes, instead of waiting for the
// periodic timer. Blocks until the round completes. Use it to
// converge on demand (e.g. tests, or a manual "sync now"); normal
// operation does not need it — periodic and reactive sync keep the
// space up to date on their own.
SyncHeads(ctx context.Context) error
// TreeHeads returns the space's current sync frontier: the head
// change ids of every live (non-deleted) tree known locally — one
// entry per tree, materialized trees and heads-only stubs alike
// (under selective sync every tree head-syncs even when its
// content is not pulled), including system trees such as settings.
// A causal-attestation primitive for embedders: a peer holding
// every head of another peer's entry has seen at least that peer's
// change set for the tree. Used by the filenode-v2 broker's GC
// gate (CheckRefs).
TreeHeads(ctx context.Context) ([]TreeHeads, error)
}
Space is the caller-facing interface to one space. Obtained from Service.Create / Join / Derive / Get. Middleware holds Space handles for the lifetime of use; the SDK manages underlying ocache loading internally.
No Close method — callers do not manage space lifecycle. sdk.Close tears everything down.
type SpaceDebug ¶
type SpaceDebug struct {
SpaceId string
Peers []PeerSyncStats
}
SpaceDebug is a point-in-time snapshot of this space's outbound headsync activity. Returned by DebugAPI.Space.
Peers lists one row per responsible peer we've completed at least one diff round against since the SDK started. In-memory only — cleared on restart.
type SpaceInfo ¶
type SpaceInfo struct {
Id string
Type string // on-wire header type (any.space, any.onetoone, …; the tech type only on the tech handle)
// SpaceType is the app-level tag set via DeriveRequest.SpaceType,
// read from the in-space spaceIndex. Independent of the header Type;
// use it for client-side classification/filtering. Untagged spaces
// carry SpaceTypeAny.
SpaceType string
// Author is the space owner's account identity, resolved from the
// ACL. Best-effort: empty when the ACL is not loadable.
//
// For a 1-1 (SpaceTypeOneToOne) space the ACL owner is a synthetic
// shared key nobody holds, so Author instead carries the OTHER
// participant's account identity — the friend this 1-1 is with.
// Available even while the space is only pending (not materialized),
// so clients can identify and resolve the friend's profile from the
// space list directly.
Author string
Name string
Description string
IconCID string
Status Status
// OwnRole is this account's ACL permission in the space, mirrored
// from ACL state onto the tech-space row by the per-space ACL
// mirror (same trigger model as PushKeys: one pass at space load
// plus a kick per applied ACL record). PermissionNone until the
// mirror first runs — notably on rows whose space was never loaded
// by this device — so treat "none" on an active space as
// "unknown yet", not a verdict; Space.Members().Me stays the
// authoritative per-space read.
//
// For a 1-1 (SpaceTypeOneToOne) space the ACL owner is the
// synthetic shared key, so both participants mirror the role the
// ACL actually grants them — never PermissionOwner.
OwnRole Permission
CreatedAt time.Time
// Settings is the account-private, client-owned per-space settings
// object: free-form keys with scalar values (string / float64 /
// bool — numbers decode as float64, JSON semantics). Written per
// key via Service.SetSettings; synced across the account's devices
// through the tech space, never visible to other space members.
// Nil when never written.
Settings map[string]any
// PushKeys is the space's push-notification key material, mirrored
// from ACL state so clients can cache it and decrypt push payloads
// while the SDK process is down (see docs/tech-space.md
// § "Space Index"). Nil until the per-space mirror has run —
// notably on a joiner whose access is still pending (no read key
// yet) and on rows whose space was never loaded by this device.
PushKeys *PushKeys
// Derived marks a space created by the account's own
// Service.Derive — Delete refuses it (see ErrIsDerivedSpace).
// Always false on created / joined / tracked / 1-1 spaces.
Derived bool
// P2PAdvertise is the per-space p2p advertising switch (on by
// default): while on, this account's devices publish their global p2p
// record into the space so the other members can dial them; off
// hides them from those members. Own devices find each other through
// the account's discovery record either way. Synced account-wide;
// written via Space.SetP2PAdvertise.
P2PAdvertise bool
}
SpaceInfo is a point-in-time snapshot of space metadata. Returned by Service.List and Space.Info; does not auto-update — subscribe via Service.Subscribe for live changes.
type SpaceListEvent ¶
SpaceListEvent is delivered to Service.Subscribe callbacks.
type SpaceSyncStatus ¶
type SpaceSyncStatus struct {
SpaceId string
State SyncState
Synced int
Total int
NetworkPeers int
LocalPeers int
GlobalPeers int
P2P P2PState
LastSyncedAt time.Time
}
SpaceSyncStatus is the per-space rolled-up sync state. Returned by Service.Status and delivered on Service.SubscribeStatus events.
Counts:
- Total = number of regular objects known locally (the per-space `objects` collection). Excludes ACL / settings / spaceIndex / members system — non-user-visible trees.
- Synced = Total - count of trees the tracker currently holds in SyncStateSyncing. A tree that has never produced a status hook is treated as Synced — no evidence of work needed.
- NetworkPeers = responsible sync nodes with a live connection.
- LocalPeers = local-network (LAN) peers sharing this space with a live connection.
- GlobalPeers = internet-wide (relay / hole-punched) peers sharing this space with a live connection. P2P summarizes LocalPeers and GlobalPeers as one state.
type Stamp ¶
Behavioral dataset-schema vocabulary, aliased from the handler package (one vocabulary for compiled-in and runtime declarations).
type Status ¶
type Status uint8
Status is the combined local+remote state of a space.
const ( StatusUnknown Status = iota StatusActive // StatusJoining is a request-to-join pending the owner's approval. // Synced account-wide: every device of the account reads it, none // materializes the space, and the device that observes the // acceptance loads it and flips the row to StatusActive for the // rest. Withdraw with Service.CancelJoin (any device). StatusJoining StatusLeaving // local delete in flight // StatusDeleted is a deleted row: the synced tombstone Delete writes, // or a join that ended without membership (the owner declined, or // Service.CancelJoin withdrew it) — synced too, so the account's // devices converge on it. The latter is re-joinable: Join with a // valid invite returns it to StatusJoining, and a direct add by the // owner surfaces it as StatusInvitePending. StatusDeleted StatusRemoteDead // network says space no longer exists // StatusOneToOnePending is an incoming 1-1 (direct) space awaiting // local approval. The space is not materialized or synced until // accepted — Accept it via Service.AcceptOneToOne / OneToOne, or // reject it via Service.DeclineOneToOne. Device-local: discovery is // per-device, so the prompt is approved/declined per device until a // decline (which is synced account-wide). StatusOneToOnePending // StatusOneToOneDeclined is a 1-1 space the user declined. Synced and // sticky across the account's devices: the request never re-surfaces // from the discovery layer. An explicit OneToOne(peer) overrides it. StatusOneToOneDeclined // StatusInvitePending is a regular space another account added us to // directly (ACL AddAccounts). We are already a full ACL member; // approval is a local materialization gate — nothing is downloaded // until accepted. Synced account-wide (any device can act on it). // Accept via Service.AcceptInvite, reject via Service.DeclineInvite. StatusInvitePending // StatusInviteDeclined is a direct-add invite the user declined. // Synced, sticky, non-terminal: a later AcceptInvite overrides it. No // ACL write happens on decline — the account remains an ACL member. StatusInviteDeclined // StatusGuestRevoked is a guest-key space whose shared guest identity // was removed from the ACL (the owner revoked public access). The // local copy stays readable; new content no longer arrives (the read // key rotated away). Device-local and non-terminal: it self-heals // back to Active if a fresh ACL shows the guest identity active // again. Remove the space with Service.Delete when no longer wanted. StatusGuestRevoked )
type SubRecord ¶
SubRecord is one record's worth of state inside a SubscriptionEvent. Doc carries the full post-apply value (cloned, safe to retain past the event). Ops carries the per-field $set / $unset ops from the triggering change — same payload shape as EventOp on the raw event stream, so callers can apply atomic updates against a local mirror without re-materialising the whole record.
type SubscriptionEvent ¶
type SubscriptionEvent struct {
VersionId crdt.VersionId
Added []SubRecord
Updated []SubRecord
Removed []RemovedRecord
}
SubscriptionEvent is one batch of windowed transitions delivered to a QuerySubscription. It groups every record-level change observed during one CRDT apply: records that entered the visible window (Added), records already in the window whose state changed (Updated), and records that left the visible window (Removed).
Removed carries every id that left the visible window between apply ticks, each tagged with a RemoveReason. Branch on RemoveDeleted to tell "the object is gone" (drop it for good) from RemoveFilteredOut / RemoveDisplaced ("it left your result set but still exists" — a Snapshot or Query.One would still return it). See RemoveReason.
VersionId carries the per-change DAG order of the underlying CRDT apply this event was derived from. Useful for consumers that want fence-and-replay semantics (e.g. "I've already processed up to version X — discard anything ≤ X"). Note VersionIds are locally-scoped: each peer assigns its own; don't compare across peers.
Total is intentionally absent — the live counter is not maintained. Callers who need a refreshed count call Snapshot.
type SyncState ¶
type SyncState uint8
SyncState is the high-level state of a space or single object. Used in both SpaceSyncStatus.State and ObjectSyncStatus.State.
Rollup priority (highest match wins, see Service.Status):
- Error — incompatible network / needs update
- Offline — no responsible node reachable
- Syncing — at least one tracked tree has pending heads
- Synced — every tracked tree converged with a responsible node
- Unknown — bootstrap; no hooks fired yet, no peers polled yet
type SyncStatusAPI ¶
type SyncStatusAPI interface {
// Space returns the rolled-up state for this space. Cheap; safe
// to call on every UI render.
Space() SpaceSyncStatus
// Object returns the state for a specific objectId. Unknown ids
// return ObjectSyncStatus{State: SyncStateUnknown}.
Object(objectId string) ObjectSyncStatus
// SubscribeObject delivers an ObjectSyncStatus event for
// objectId on every state flip. cb runs synchronously on the
// dispatcher goroutine — keep it small or hand off.
SubscribeObject(objectId string, cb func(ObjectSyncStatus)) (cancel func())
}
SyncStatusAPI exposes per-space sync state and per-object subscriptions for one space. Obtained via Space.SyncStatus().
Account-wide subscription lives on space.Service (SubscribeStatus) — middleware rendering a space list should subscribe there, not loop over per-space handles.
type TouchedRecord ¶
TouchedRecord names one record a change touched with its op kinds (e.g. "$set", "$inc", "$delete").
type TreeHeads ¶
TreeHeads is one tree's current heads as reported by Space.TreeHeads: the frontier element for that tree. Heads are change ids.
type TypeCreateParams ¶
type TypeCreateParams struct {
Name string
Description string
IconCID string
// XKey is an optional stable, caller-side "programmatic" name for
// the type (e.g. for generated client code mapping). Client-set,
// not unique, not enforced by the SDK. Unlike Name/Description it
// is stored in the meta-type's own namespace (`type.xkey`), so
// only rows carrying the type marker can hold one.
XKey string
// Layout seeds the rendering metadata — see TypeInfo. Mutable
// through Patch.
Layout map[string]any
// Hidden and Meta seed the listing flag and the consumer flag bag —
// see TypeInfo. Both mutable through Patch.
Hidden bool
Meta map[string]any
}
TypeCreateParams is the input to TypesAPI.Create.
type TypeInfo ¶
type TypeInfo struct {
Id string
Name string
Description string
IconCID string
// XKey is the optional caller-side "programmatic" name set at
// Create, stored at `type.xkey` on the type object. Empty if
// unset.
XKey string
// Layout is how the type's header and parts compose — a slug plus
// config in the x-format shape ({type, config}), opaque to the
// SDK. Lives in the meta-type's namespace (`type.layout`).
Layout map[string]any
// Hidden keeps the type out of default listings and pickers: a
// client shows it only on request. A bundle root asks for it
// (EnsureBundleRequest.Hidden) when it exists to host its bundle's
// records rather than to be attached elsewhere; a registered type
// declares it (handler.Type.Hidden). `type.hidden`.
Hidden bool
// Meta is the open bag of consumer flags on the type — one scalar
// (string, bool, number) per single-level key, written per key so
// concurrent writers merge. Opaque to the SDK; consumers read the
// keys they own (an indexer's `index`, a client's tags).
// `type.meta`.
Meta map[string]any
// BuiltIn marks the synthetic types — `any`, `spaceIndex`, `type`,
// `collection` and every caller-registered type (immutable,
// always-present). User types return false.
BuiltIn bool
}
TypeInfo is a point-in-time snapshot of a type object.
type TypePatch ¶
type TypePatch struct {
Name *string
Description *string
IconCID *string
Layout map[string]any
ClearLayout bool
Hidden *bool
// Meta patches the flag bag per key: a scalar value sets the key,
// a nil value unsets it. Keys not named are untouched, so writers
// on different devices touching different keys merge.
Meta map[string]any
}
TypePatch is the input to TypesAPI.Patch. Nil pointers keep the current value; an empty string clears a text field. Layout replaces the whole layout object when non-nil; ClearLayout removes it.
type TypesAPI ¶
type TypesAPI interface {
PropertyDefsAPI
List(ctx context.Context) ([]TypeInfo, error)
Get(ctx context.Context, typeId string) (TypeInfo, error)
// Create a new user-defined type in this space. Returns the new
// type object's id.
Create(ctx context.Context, params TypeCreateParams) (typeId string, err error)
// Delete a type. Objects naming it in `any.type` keep the
// reference (orphan), per docs §"read tolerance".
Delete(ctx context.Context, typeId string) error
// Patch edits a user type's display and rendering metadata: name,
// description, icon, layout, hidden, meta. Absent fields keep
// their value. Registered built-ins refuse (ErrTypeRegistered); a
// collection id refuses (ErrNotAType).
Patch(ctx context.Context, typeId string, patch TypePatch) error
// Parts returns the type's parts with their datasets (the compiled
// view — duplicate keys folded, orphan records dropped, invalid
// datasets flagged).
Parts(ctx context.Context, typeId string) ([]PartDef, error)
// AddPart declares a part with its initial datasets in one change.
// Returns the part's stable id. The key is pinned; the display
// slice patches via PatchPart; datasets evolve via AddDataset /
// RemoveDataset on the part.
AddPart(ctx context.Context, typeId string, draft PartDraft) (partId string, err error)
// PatchPart edits a part's mutable leaves: name, icon, pos, hidden,
// ui (written whole), uses. The key is pinned (ErrPinnedField).
PatchPart(ctx context.Context, typeId, partId string, patch DatasetDefPatch) error
// RemovePart tombstones a part and every dataset declared under it.
// Record data is NOT cleaned up (the RemoveProperty stance);
// subsequent writes to the datasets drop once peers apply the
// removal, and a shared dataset's removal only withdraws this
// type's ownership of the canonical collection.
RemovePart(ctx context.Context, typeId, partId string) error
// Datasets returns the type's dataset definitions across every part
// (the compiled view — orphan/invalid records folded out).
Datasets(ctx context.Context, typeId string) ([]DatasetDef, error)
// AddDataset declares a dataset on an existing part. The definition
// syncs like any space data; peers register the dataset (the
// generic schema handler for records, the module's handler
// otherwise) as it applies. Returns the definition's stable id.
// Behavioral parts of the declaration (key, module, shared, id
// rule, delete gate, field kinds/flags) are pinned — remove and
// re-add to change them; display parts patch via PatchDataset.
AddDataset(ctx context.Context, typeId, partId string, draft DatasetDraft) (datasetDefId string, err error)
// AddDatasetField appends a field to an existing records dataset
// (additive evolution). Returns the field definition's id. Additive
// fields cannot be Required — validation always runs against the
// current schema, so a required field added later would reject the
// dataset's own history on fresh devices. Declare required fields
// at AddDataset. A module-served dataset refuses (ErrModuleOwned).
AddDatasetField(ctx context.Context, typeId, datasetDefId string, draft DatasetFieldDraft) (fieldDefId string, err error)
// RemoveDataset drops a dataset definition. Existing record data is
// NOT cleaned up (the RemoveProperty stance); subsequent writes to
// the dataset drop once peers apply the removal.
RemoveDataset(ctx context.Context, typeId, datasetDefId string) error
// RemoveDatasetField drops one field definition. Existing values
// stay stored; subsequent writes to the field are rejected as
// undeclared (non-dynamic datasets).
RemoveDatasetField(ctx context.Context, typeId, fieldDefId string) error
// PatchDataset edits a definition's mutable leaves: displayName,
// description, name (field records' display label), search.title,
// search.text (a field key string or a non-empty array of unique
// keys; single-element arrays canonicalize to the bare string on
// the wire, and clearing the mapping is Unset's job), search.scope.
// Pinned paths are rejected up-front (ErrPinnedField); malformed
// values on mutable search leaves return ErrInvalidFieldValue.
PatchDataset(ctx context.Context, typeId, defId string, patch DatasetDefPatch) error
// PatchDatasetField edits one field definition's mutable leaves:
// name, description, x-format and every path under it. The
// behavioral parts (key, kind, shape, scope, required, mutableBy,
// stamp) are pinned (ErrPinnedField). Unknown fieldDefId →
// ErrNotFound.
PatchDatasetField(ctx context.Context, typeId, fieldDefId string, patch DatasetDefPatch) error
}
TypesAPI manages type objects inside a space.
A type is what an object IS: property definitions, parts (the datasets it renders) and a layout. An object has exactly one type, named in `any.type`; a type object itself carries the reserved marker `__type__` there (it has no type of its own). Property values for an object live on the per-space objects row under `<typeId>.<propId>` (see PropertiesAPI). What an object is filed UNDER is a collection — see CollectionsAPI.
Built-in types (`any`, `type`, `collection`, `spaceIndex`) are derived on demand — callers don't create them. List returns them, and every registered type, alongside user-defined types.
type UnreadChange ¶
type UnreadChange struct {
ObjectId string
// Dataset the change applied to (a tracked dataset).
Dataset string
// ChangeId is the DAG change (cross-peer identity); VersionId is
// its peer-local order key — for a record-creating change it
// equals the record's `_ver.id`, so entries join to records with
// no extra lookup.
ChangeId string
VersionId crdt.VersionId
AddSeq uint64
ApplySeq uint64
// RecordIds are the records the change touched; Tags the
// classifier's labels ("message", "mention", ...).
RecordIds []string
Tags []string
// StateSeq is the feed watermark at which this entry became
// unread.
StateSeq uint64
}
UnreadChange is one currently-unread change in an object's snapshot.
type UpsertBatch ¶
type UpsertBatch struct {
ObjectId string
Dataset string
Records []UpsertRecord
// PageSize caps records per emitted change. 0 = 500.
PageSize int
TraceIds []string
}
UpsertBatch is the input to Space.Upsert — generic schema-driven batch ingest: upsert by record id, diff only declared-mutable fields against stored values, skip identical records, one change per page.
Semantics per record:
- absent → created (one multi-field $set carrying all fields);
- present → only declared-mutable fields are diffed; changed ones become single-path $set ops (per-field granularity for the schema handler); identical records emit nothing;
- present with a differing write-once field → whole-record rejection (ErrImmutableFieldChanged);
- stored tombstone → rejection (ErrRecordDeleted; ids never reuse).
Not transactional against concurrent writers: the read-diff-write window resolves by per-path LWW like any other write. The intended deployment is a single ingest writer per dataset.
Contract: a user-supplied record id must have a single writer. The id doubles as the idempotency key, and CONCURRENT creates of the same id by different members are outside the contract — creation verdicts (required fields, the creator stamp behind author gates) are taken by whichever create a replica applies first, so racing writers can observe different creators per replica. One ingest writer per dataset (or per id range) keeps this trivially true.
type UpsertRecord ¶
UpsertRecord is one row of an UpsertBatch: the caller id plus the desired field values (top-level field → value; values follow Op.Value conventions).
type UpsertRejection ¶
UpsertRejection reports one rejected record: its batch index, id, and cause (errors.Is-matchable).
type UpsertResult ¶
type UpsertResult struct {
Pages []ModifyResult
Created int
Updated int
Skipped int
Rejections []UpsertRejection
}
UpsertResult aggregates the batch outcome. Pages holds one ModifyResult per emitted change, in page order (pages with nothing to write are absent).
type Variant ¶
type Variant string
Variant selects which representation of a file to open. Variants (thumbnails etc.) are sibling payload rows tagged in their sealed meta; resolution lands with SYN-30 — only VariantOriginal is accepted until then.
const VariantOriginal Variant = ""
VariantOriginal is the file's primary content.
type Version ¶
type Version = string
Version history (docs/version-history-proposal.md). The public version handle is a ChangeId — the content-hash CID of a DAG change, stable across peers and restarts. "State at version X" is the projection of exactly X's causal past, inclusive (§3.2): stable on every device, at the cost that a historical view may include concurrent changes the user hadn't seen at the time.
type VersionId ¶
VersionId is the lexicographically-sortable local ordering key maintained by any-sync's tree storage (= its orderId). It is peer-local: scoped to one peer's view of one object tree, NOT a globally-consistent identity.
See internal/crdt.VersionId for the full contract. This alias makes the type available to middleware without exposing the internal package.
Source Files
¶
- acl.go
- aggregate.go
- bundles.go
- changeindex.go
- collections_doc.go
- debug.go
- devices.go
- doc.go
- files.go
- history.go
- identities.go
- identity.go
- indexer.go
- info.go
- invite.go
- members.go
- modify.go
- objects.go
- payloads.go
- properties.go
- pubsub.go
- push.go
- query.go
- readstate.go
- schema.go
- service.go
- space.go
- subscribe.go
- syncstatus.go
- types.go
- upsert.go
- version.go