Documentation
¶
Overview ¶
Package typetype is the built-in `type` meta-type — the shape of type objects themselves. Ships the description of the meta-type and the handler that validates writes to a type object's `properties` dataset (the property-definition records).
Directory is internal/types/type/; the package is declared `typetype` because `type` is a Go keyword and can't be a package name.
`type` ships as a derived object in every space (well-known id).
Index ¶
- Constants
- Variables
- func CheckSearchLeafValue(path []string, payload *anyenc.Value) error
- func IsDatasetDefPinnedPath(path []string) bool
- func IsDatasetFieldPinnedPath(path []string) bool
- func IsPartPinnedPath(path []string) bool
- func IsPinnedPath(path []string) bool
- func ValidateKey(what, key string) error
- type BuiltInProperty
- type DatasetDefsHandler
- func (DatasetDefsHandler) BeforeCreate(ctx *crdt.ChangeCtx, rec *crdt.RecordChange, sink *crdt.Sink) error
- func (DatasetDefsHandler) BeforeDelete(ctx *crdt.ChangeCtx, rec *crdt.RecordChange, sink *crdt.Sink) error
- func (DatasetDefsHandler) BeforeModify(_ *crdt.ChangeCtx, _ *crdt.RecordChange, op *crdt.Op, _ *crdt.Sink) error
- func (DatasetDefsHandler) Init(_ context.Context) error
- type PropertyHandler
- func (PropertyHandler) BeforeCreate(ctx *crdt.ChangeCtx, rec *crdt.RecordChange, sink *crdt.Sink) error
- func (PropertyHandler) BeforeDelete(ctx *crdt.ChangeCtx, rec *crdt.RecordChange, sink *crdt.Sink) error
- func (PropertyHandler) BeforeModify(ctx *crdt.ChangeCtx, rec *crdt.RecordChange, op *crdt.Op, sink *crdt.Sink) error
- func (PropertyHandler) Init(_ context.Context) error
Constants ¶
const ( DefKindPart = "part" // part record: one per part of the type DefKindDataset = "dataset" // head record: one per dataset of a part DefKindField = "field" // field record: one per field of a records dataset )
Discriminator values of the pinned `def` field.
const ( DefFieldDef = "def" // discriminator, pinned DefFieldModule = "module" // module slug (head), pinned DefFieldPart = "part" // owning part record id (head), pinned DefFieldDynamic = "dynamic" // bool (head), pinned DefFieldIdRule = "idRule" // "auto"/"user" (head), pinned ("id" is a reserved head) DefFieldIdPattern = "idPattern" // RE2 (head), pinned DefFieldIdMaxLen = "idMaxLen" // number (head), pinned DefFieldDeleteBy = "deleteBy" // "anyone"/"author" (head), pinned DefFieldSkipHistory = "skipHistory" // bool (head), pinned DefFieldSearch = "search" // {title,text,scope} (head); leaves mutable, text string-or-array DefFieldDisplayName = "displayName" // human label (head), mutable DefFieldDataset = "dataset" // owning head record id (field), pinned DefFieldStamp = "stamp" // "creator"/"createTime"/"modifyTime" (field), pinned DefFieldRequired = "required" // bool (field), pinned DefFieldMutableBy = "mutableBy" // "never"/"author"/"any" (field), pinned )
Dataset-def record field names. Part records use the Part* set plus FieldKey / FieldName; head records the Def* set plus FieldKey; field records reuse FieldKey/FieldKind/FieldName/FieldDescription/FieldItems/ FieldProperties from the property-record vocabulary plus the Field* behavioral set below.
const ( PartFieldIcon = "icon" // string PartFieldPos = "pos" // string, clients sort parts by it PartFieldHidden = "hidden" // bool, not shown by default PartFieldUI = "ui" // object {type, config}, written whole PartFieldUses = "uses" // array of dataset keys of this type )
Part record fields — the display slice a client renders. All mutable; `key` (FieldKey) is the only pinned one.
const ( SearchKeyTitle = "title" SearchKeyText = "text" SearchKeyScope = "scope" )
Sub-keys of the head `search` object — mutable leaves (broad `search` replaces are pinned, the leaves mutate freely). `title` and `scope` are scalar strings; `text` is a bare field key or a non-empty array of field keys.
const ( ShortIdFieldChangeId = "changeId" // the changeId the shortId was derived from ShortIdFieldPropId = "propId" // the property record this shortId stamps (added or removed) ShortIdFieldKind = "kind" // on-wire kind label, present on add rows ShortIdFieldRemoved = "removed" // bool, true on removal rows ShortIdFieldDefId = "defId" // the dataset-def record this shortId stamps ShortIdFieldSrc = "src" // discriminator: absent = properties, "datasets" = dataset defs )
ShortId row field names.
const ( MetaTypeMarker = "__type__" TypeId = "type" )
MetaTypeMarker is the reserved value every type object carries in `any.type` — what distinguishes a type object from a regular one (see spaceobjects.LiveTypeRowsFilter). A definition object has no type of its own: the slot holds the marker.
TypeId is the meta-type's id: the namespace its type-only property values live under (`record.type.xkey`) and the id surfaced through Space.Types(). The two differ because a `_`-prefixed top-level field is protocol-owned (crdt.validatePath), so the marker cannot double as a storage namespace.
Both are reserved — user-derived type ids are content-addressable and never produce either string.
const ( Name = "Type" Description = "A type — defines the properties, parts and layout of the objects that have it" )
Display metadata for the `type` meta-type object.
const ( FieldHiddenProp = "hidden" FieldMetaProp = "meta" )
`type.hidden` keeps a type out of default listings and pickers (self-typed bundle roots carry it); `type.meta` is the open bag of consumer flags — one scalar per key, written per key so writers touching different keys merge (`meta.index`, a client's own tags).
const ( FieldKey = "key" // user-facing stable identifier (e.g. "actors") FieldKind = "kind" // "string"/"number"/"boolean"/"null"/"array"/"object"/"datetime" FieldScope = "scope" // "synced"/"account"/"local" — write/sync class, pinned FieldName = "name" // human label, mutable FieldDescription = "description" // mutable FieldXKey = "x-key" // caller-side handle, mutable FieldItems = "items" // recursive sub-shape for arrays FieldProperties = "properties" // recursive sub-shape for objects FieldRequired = "required" // []string, mutable per docs/data-structure.md FieldMeta = "meta" // opaque consumer flag map (string→string), mutable FieldXFormat = "x-format" // opaque descriptor object, mutable — see below )
Property-record field names. The shape is hardcoded in Go (no JSON Schema applies on this dataset — see docs/types-properties-proposal.md § "Schema format — decision").
const DatasetDefs = "datasets"
DatasetDefs is the dataset on a type object holding its dataset definition records. On disk the collection is `<typeId>_datasets`.
const DatasetDefsHandlerVersion = "typeDatasetHandler-v1"
DatasetDefsHandlerVersion is the DataVersion stamped on changes to the DatasetDefs dataset itself (hardcoded, like HandlerVersion — definition writes are never gated on their own schema state).
Known mixed-fleet limitation: this string is deliberately NOT parseable by the spaceobjects DataVersion gate (legacy handler-version strings pass through unconstrained — see gateFor), so bumping it CANNOT park new wire forms on old replicas. A peer that predates the array text form sheds an array-form `search.text` $set (its handler admits only scalar strings) and never replays it, diverging the head record until re-written. Accepted pre-release; a real fix needs a version pair the gate parses (and old drainers can satisfy), which is a protocol change.
const DatasetDefsLocalVersion = 4
DatasetDefsLocalVersion is this handler's LOCAL logic version (HandlerReg.Version): bumped when a validation change means an already-materialized set of dataset definitions would come out different, so the SDK replays the type object's tree. Like the property handler's, it is also the recovery path for definitions an older build dropped because it could not validate them.
v2: `datetime` is a kind, so a stamped time field validates. v3: field records carry an opaque `x-format` (an object, created whole, mutable); head records carry none. v4: parts. A head is keyed (`key`, pinned) and bound to a part and a module (`part`, `module`, `shared`, pinned); the `collection` field is gone — the collection is computed at compile.
const DatasetPropertyDefs = "properties"
DatasetPropertyDefs is the dataset on a type object that holds its property-*definition* records (id, name, kind, ...). On disk the collection is named "properties"; the Go identifier was chosen to disambiguate from property *values*, which live in a separate dataset called "objects" (properties.Dataset, handled by properties.SystemPropertiesHandler). Every object registers both handlers — only type objects actually write to this one.
const FieldLayoutProp = "layout"
Rendering metadata a type object carries in its own namespace: `type.layout` is the type's layout descriptor ({type, config}, written whole, opaque to the SDK).
const FieldXKeyProp = "xkey"
FieldXKeyProp is the property id of the meta-type's `xkey` — the caller-side programmatic handle for the type itself, stored at `record.type.xkey`. Distinct from FieldXKey (`x-key`), which is the same idea one level down, on a property DEFINITION record.
const HandlerVersion = "typePropertyHandler-v1"
HandlerVersion is the DataVersion string stamped onto every change this handler emits against the `properties` dataset of a type object. Per docs/types-properties-proposal.md § "Change-level DataVersion", this dataset uses a hardcoded handler-chosen identifier (not a shortId) — bump the suffix if the validation rules ever change in a way that must reject stale writers.
const PropertyHandlerLocalVersion = 3
PropertyHandlerLocalVersion is this handler's LOCAL logic version (HandlerReg.Version) — bumped when a validation change means an already-materialized set of definitions would come out different, so the SDK replays the type object's tree (docs/versioning.md).
It is also how a peer recovers definitions its previous build dropped: the wire DataVersion deliberately stays put (bumping it would park every property change on peers that don't know the new version — see the Properties note above), so an older build rejects a definition it cannot validate, and the replay after the upgrade applies it.
v2: `datetime` is a kind, and the date formats accept it. v3: the typed `format` object is gone (no format→kind coupling, no pinned `format.type`); `x-format` must be an object, created whole.
const ShortIdSrcDatasets = "datasets"
ShortIdSrcDatasets marks rows projected by DatasetDefsHandler. Property rows carry no `src` — the absent value is the legacy discriminant, so old readers keep working. The gate never inspects fields (KnownShortId is a FindId), so mixing both streams in one collection is free; LatestShortId then gates data changes against the type's WHOLE schema state (properties + dataset defs), which is strictly safer.
const ShortIdsDataset = "shortIds"
ShortIdsDataset is the name of the sibling collection registered on every type object's Controller alongside DatasetPropertyDefs. One row per important change to the type's properties dataset.
docs/types-properties-proposal.md § "Known-shortIds set":
{ shortId, versionId, changeId }
Append-only in v1; no GC. The shortId record id is base58(xxh3-64(changeId)) — the same derivation as crdt.DeriveRecordId, since a content-addressable changeId is the only thing we hash.
const WellKnownDeriveSeed = "builtin:type"
WellKnownDeriveSeed mints the same `type` type object id on every peer.
Variables ¶
var ErrBadDatasetDef = errors.New("typetype: invalid dataset definition record")
ErrBadDatasetDef indicates a structurally invalid dataset-definition record. Wraps crdt.ErrValidation.
var ErrBadScope = errors.New("typetype: property `scope` must be one of synced/account/local")
ErrBadScope indicates a property record declared an unknown scope label, or the reserved "derived" scope (SDK built-ins only). Wraps crdt.ErrValidation so callers can match either.
var ErrBadXFormat = errors.New("typetype: `x-format` must be an object created whole")
ErrBadXFormat indicates a structurally malformed `x-format` in a creation change: not an object, or written through dotted `x-format.*` keys instead of as one whole object. Shared by property and dataset-field records. Wraps crdt.ErrValidation.
var ErrMissingKind = errors.New("typetype: property record requires `kind`")
ErrMissingKind indicates a property record was created without a `kind` field. Wraps crdt.ErrValidation so callers can match either.
var Properties = []BuiltInProperty{ {Id: FieldXKeyProp, Name: "XKey", Kind: schema.KindString, Scope: schema.ScopeSynced, Description: "Programmatic handle of the type; consumers keep it unique per space."}, {Id: FieldLayoutProp, Name: "Layout", Kind: schema.KindObject, Scope: schema.ScopeSynced, Description: "Layout descriptor of the primary type: {type, config}."}, {Id: FieldHiddenProp, Name: "Hidden", Kind: schema.KindBoolean, Scope: schema.ScopeSynced, Description: "Keeps the type out of default listings and pickers.", XFormat: map[string]any{"type": "checkbox"}}, {Id: FieldMetaProp, Name: "Meta", Kind: schema.KindObject, Scope: schema.ScopeSynced, Description: "Consumer flags, one scalar per key."}, }
Properties lists the meta-type's hardcoded property definitions — the values that are meaningful only on a type object. They live in the `type` namespace rather than `any` so the membership check in properties.SystemPropertiesHandler fences them off: a row that doesn't carry the marker cannot hold them at all.
The `objects` DataVersion is deliberately NOT bumped for the move off `any.xkey`: a peer that doesn't know a version parks every change for that dataset, so bumping would halt all property sync with older peers to protect one field. Mixed versions instead drop the unknown-namespace op per-op — the type's other metadata still applies, and its xkey resolves on upgraded peers only.
Functions ¶
func CheckSearchLeafValue ¶
CheckSearchLeafValue validates a search.* leaf's $set payload: `title`/`scope` must be scalar strings, `text` a bare string or a non-empty array of unique non-empty string keys (both wire forms). Shared with the client-side PatchDataset preflight; returns a plain error — callers wrap with their own sentinel.
The array branch deliberately re-checks entry types instead of decoding through schema.SearchTextFromAnyenc: the parser is tolerance-biased (a non-string entry becomes "" for the compile fold to flag), while a write gate owes the author the precise "entries must be strings". A bare "" stays admissible here — older handlers accepted it, so rejecting it at apply time would diverge on old-authored changes; the PatchDataset preflight is where the empty spellings get refused.
func IsDatasetDefPinnedPath ¶
IsDatasetDefPinnedPath is the client-side preflight for a HEAD record (PatchDataset): the handler's rule, minus `x-format` — a head carries no descriptor (nothing reads one back), so a write there would be a permanent no-op in the DAG.
func IsDatasetFieldPinnedPath ¶
IsDatasetFieldPinnedPath reports whether a PatchDatasetField path touches pinned field-record state — everything but name, description and x-format.*.
func IsPartPinnedPath ¶
IsPartPinnedPath reports whether a PatchPart path touches pinned part-record state — everything but the display slice. `ui` is written whole (like `x-format`), so a path below it is pinned too.
func IsPinnedPath ¶
IsPinnedPath is the exported form of isPinnedPath, so the space layer (PatchProperty) can reject writes to pinned state client-side and fail the whole patch fast, rather than relying on the handler's per-op drop (which would partially apply a mixed patch).
func ValidateKey ¶
ValidateKey checks a part or dataset key: the slug rules. Shared by the create-time handler gate and the AddPart / AddDataset preflights.
Types ¶
type BuiltInProperty ¶
type BuiltInProperty struct {
Id string
Name string
Kind schema.Kind
Scope schema.Scope
// Description and XFormat are the descriptive slice: a display
// description and the opaque descriptor bag (docs/data-structure.md § The
// `x-format` descriptor). Surfaced by Types().Properties and
// dataset discovery like a user definition's; never interpreted
// or enforced by the SDK.
Description string
XFormat map[string]any
}
BuiltInProperty is one hardcoded property definition. Same shape as anytype.BuiltInProperty so the types registry surfaces built-ins uniformly with user-defined types.
type DatasetDefsHandler ¶
type DatasetDefsHandler struct{}
DatasetDefsHandler validates ops on a type object's `datasets` dataset and projects shortId rows on every important change (def added / removed), so the existing DataVersion gate parks data changes written against schema state this replica hasn't applied yet.
All validation is record-local and stateless (see PropertyHandler for the convergence rationale): cross-record consistency — orphan field records, author rules without a creator stamp — is resolved deterministically at catalog compile, never here.
func (DatasetDefsHandler) BeforeCreate ¶
func (DatasetDefsHandler) BeforeCreate(ctx *crdt.ChangeCtx, rec *crdt.RecordChange, sink *crdt.Sink) error
BeforeCreate validates a definition record's creation shape and projects the "added" shortId row.
func (DatasetDefsHandler) BeforeDelete ¶
func (DatasetDefsHandler) BeforeDelete(ctx *crdt.ChangeCtx, rec *crdt.RecordChange, sink *crdt.Sink) error
BeforeDelete classifies the removal as an important change (removed definitions must gate stale writers) and projects the removal row. Record data under a removed dataset is not cleaned up in v1 — the RemoveProperty stance.
func (DatasetDefsHandler) BeforeModify ¶
func (DatasetDefsHandler) BeforeModify(_ *crdt.ChangeCtx, _ *crdt.RecordChange, op *crdt.Op, _ *crdt.Sink) error
BeforeModify rejects edits to pinned dataset-def state; the mutable display fields pass through and the search leaves must keep their wire shapes (scalar strings; `text` also accepts a key array).
type PropertyHandler ¶
type PropertyHandler struct{}
PropertyHandler validates ops on a type object's `properties` dataset and projects shortId rows into the sibling shortIds dataset on every "important" change (add / remove). Per docs/types-properties-proposal.md § "Schema evolution rules" and § "ShortId — derivation".
func (PropertyHandler) BeforeCreate ¶
func (PropertyHandler) BeforeCreate(ctx *crdt.ChangeCtx, rec *crdt.RecordChange, sink *crdt.Sink) error
BeforeCreate validates a creation: the record must declare a known `kind`, and — when present — a creatable `scope` (synced / account / local; absent means synced; "derived" is reserved for SDK built-ins). Mints a shortId from the change's ChangeId and projects a row into ShortIdsDataset. The apply loop stamps that row's `_ver` with this change's VersionId — same versionId as the property record itself, so the gate is consistent.
func (PropertyHandler) BeforeDelete ¶
func (PropertyHandler) BeforeDelete(ctx *crdt.ChangeCtx, rec *crdt.RecordChange, sink *crdt.Sink) error
BeforeDelete classifies the removal as an "important" change and projects a shortId row marking it. Existing record data on per-object stores isn't cleaned up — subsequent writes touching the removed property drop op-by-op via the unknown-property rule.
func (PropertyHandler) BeforeModify ¶
func (PropertyHandler) BeforeModify(ctx *crdt.ChangeCtx, rec *crdt.RecordChange, op *crdt.Op, sink *crdt.Sink) error
BeforeModify rejects edits to pinned state on an existing record: the schema-bearing fields (`key`, `kind`, `scope`, `items`, `properties`). Everything else — `name`, `description`, `x-key`, `required`, `meta.*` and every path under `x-format` — passes through with no value checks, bar one: a $set of the whole `x-format` must be an object (checkXFormatWholeSet), so the create-time shape holds for the record's life. Not an "important" change — no shortId minting — with one exception: a creation-shaped upsert landing on an existing record.
That is a concurrent duplicate create — two devices declaring a bundle property under its deterministic id while apart. The change is a create on its author's replica (BeforeCreate projected its shortId row there) and a modify everywhere else. The author stamps its later data writes with the newest shortId it knows, which may be this one, so every replica must know it too or those writes park forever: the row is projected here as well. Idempotent — the same row id on every replica — and the record itself merges as any modify does (the pinned keys are shed, the rest is identical).