protocol

package
v1.6.0-beta.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 1 Imported by: 0

Documentation

Overview

Package protocol holds the PURE, host-testable decision logic of the Gothic full-Go static core's control plane. It has no js.Value / syscall/js dependency, so it compiles and runs under the standard host toolchain and can be unit-tested without a WASM runtime. The core's main package (pkg/wasm/core-runtime, //go:build js && wasm) is a thin js.Value adapter that decodes the inbound CustomEvent, calls into here, and applies the result to the DOM bus.

Index

Constants

View Source
const (
	// DurableReqPrefix + <key>:<field> — component → core per-field WRITE
	// (DATA-PLANE binary; the frame lives in window.__gothic_topic, not detail).
	DurableReqPrefix = "gothic:durable-req:"
	// DurableBroadcastPrefix + <key>:<field> — core → component per-field REPLAY
	// (DATA-PLANE binary). Only emitted at register-time replay, never on write.
	DurableBroadcastPrefix = "gothic:durable:"
	// DurableOnlinePrefix + <key> — core → component per-key online ack
	// (CONTROL-PLANE; detail-less readiness signal after replay drains).
	DurableOnlinePrefix = "gothic:core:durable-online:"
)

Data-plane / control-plane event prefixes. These MUST stay byte-identical to the strings the TinyGo runtime hardcodes in pkg/wasm/wasm-runtime/runtime/durable.go — the runtime is a separate module and cannot import this package, so the two are kept in lockstep by hand (same as the topic names in topic.go).

View Source
const (
	// TopicReqPrefix + <key>:<field> — component → core per-field set-request
	// (DATA-PLANE binary; the frame lives in window.__gothic_topic, not detail).
	TopicReqPrefix = "gothic:topic-req:"
	// TopicBroadcastPrefix + <key>:<field> — core → components per-field state
	// broadcast (DATA-PLANE binary).
	TopicBroadcastPrefix = "gothic:topic:"
	// TopicOnlinePrefix + <key> — core → components per-key online ack
	// (CONTROL-PLANE; detail-less readiness signal).
	TopicOnlinePrefix = "gothic:core:topic-online:"
)

Data-plane / control-plane event prefixes. These MUST stay byte-identical to the strings the TinyGo runtime hardcodes in pkg/wasm/wasm-runtime/runtime/topic.go (RequestTopicSetField / ListenTopicEventField / RegisterTopicWithCore / ListenTopicCoreOnline) — the runtime is a separate module and cannot import this package, so the two are kept in lockstep by hand.

View Source
const AckPrefix = "gothic:core:ack:"

AckPrefix is the prefix of the per-scope ack event the core dispatches back to a registering component: the full event name is AckPrefix + scopeID. Owning it here keeps the decision logic and the js.Value adapter from drifting on the wire name.

Variables

This section is empty.

Functions

func DecideDurableStore

func DecideDurableStore(stored []byte, present bool, incoming []byte) bool

DecideDurableStore reports whether an inbound per-field WRITE frame should be stored, given the CURRENTLY stored frame for that (key, field) and whether any value has been stored for it before (`present`).

Durable state is a FAITHFUL survival cache, not a broadcast bus, so — unlike topics' DecideForward — it must NOT suppress an empty frame: EMPTY IS A LEGITIMATE DURABLE VALUE. A user who sets note="hi" then clears it to "" must have the cleared "" survive a teardown→re-mount, not the stale "hi". That means the store decision has to distinguish "no value ever stored" (present=false) from "empty value stored" (present=true, stored len 0):

  • present=false → always store (first write of this field, even if empty).
  • present=true → store iff the incoming frame DIFFERS from the stored one, INCLUDING a non-empty→empty transition; suppress only a true no-op (stored==incoming). Durable is store-only/private (no rebroadcast), so there is no no-op-broadcast optimization to preserve — plain change detection is both correct and sufficient.

It stays OPAQUE: it compares raw bytes with bytes.Equal and never decodes the frame. This deliberately does NOT reuse topics' DecideForward (which suppresses empties) — that behavior is correct for topics and must stay unchanged.

func DecideForward

func DecideForward(prev, next []byte) bool

DecideForward reports whether an inbound per-field frame should be stored and rebroadcast, given the previously stored frame for that (key, field).

It is the byte-compare diff that suppresses no-op rebroadcasts. It is OPAQUE by construction: it compares raw bytes with bytes.Equal and NEVER decodes or interprets the frame. An empty/nil next is never forwarded (nothing to store); otherwise forward when there is no prior value or the bytes changed. prev==nil is handled naturally by bytes.Equal (a nil prev never equals a non-empty next).

func DurableBroadcastEvent

func DurableBroadcastEvent(key, field string) string

DurableBroadcastEvent is the per-field replay event the core re-forwards to the (re)mounting component (data-plane). Verbatim frame forward; no decode.

func DurableOnlineEvent

func DurableOnlineEvent(key string) string

DurableOnlineEvent is the per-key control-plane online ack the core announces after replaying the durable key's stored per-field state to a (re)mounting component.

func DurableReqEvent

func DurableReqEvent(key, field string) string

DurableReqEvent is the per-field write event a durable component dispatches to the core (data-plane). Routing key only — the core never parses the frame.

func TopicBroadcastEvent

func TopicBroadcastEvent(key, field string) string

TopicBroadcastEvent is the per-field state event the core rebroadcasts to every consumer of key (data-plane). Verbatim frame forward; no decode.

func TopicOnlineEvent

func TopicOnlineEvent(key string) string

TopicOnlineEvent is the per-key control-plane online ack the core announces after replaying the topic's current per-field state to a (re)joining consumer.

func TopicReqEvent

func TopicReqEvent(key, field string) string

TopicReqEvent is the per-field set-request event a component dispatches to the core (data-plane). Routing key only — the core never parses the frame.

Types

type Decision

type Decision struct {
	// Record is true when the message is well-formed and should be recorded +
	// acked. When false, the message is ignored (no record, no ack) and every
	// other field is zero.
	Record bool
	// RecordKey is the store key (the schemaID) under which the core records the
	// {scopeId, schemaId, schema} entry.
	RecordKey string
	// AckEvent is the document event name the core dispatches the ack on.
	AckEvent string
	// AckScopeID / AckSchemaID are the ack detail fields (and the values the core
	// writes back into the record, echoing the request).
	AckScopeID  string
	AckSchemaID string
}

Decision is the core's response to an inbound gothic:core:register message. It is intentionally value-only (no js.Value) so it is host-testable.

func DecideRegister

func DecideRegister(scopeID, schemaID string) Decision

DecideRegister is the pure record→ack decision for an inbound registration.

A message with a non-empty schemaID is recorded under schemaID and acked to the registering scope (AckPrefix + scopeID). A message with an EMPTY schemaID is ignored — there is no key to record it under and no way to reference it later — so it produces the zero Decision (Record=false).

The registered SCHEMA descriptor is deliberately NOT a parameter: the decision never depends on the schema's content, which is what makes the core's handling OPAQUE. The adapter carries the schema value straight from the request into the stored record without this function ever seeing it.

type DurableRegisterDecision

type DurableRegisterDecision struct {
	// NewFields is the subset of the incoming field list the core has NOT yet
	// subscribed a per-field WRITE listener for. Durable fields register
	// INCREMENTALLY — a component may call DurableObserve several times, each
	// registering one field under the same durable key — so unlike a topic (whose
	// full field set arrives in one register) the core subscribes the delta and
	// leaves existing subscriptions untouched. It is ROUTING metadata only; the
	// core stores/forwards each field's bytes opaquely and never interprets them.
	NewFields []string
	// OnlineEvent is the per-key online ack the core announces after replay (empty
	// only when key is empty, which is ignored entirely).
	OnlineEvent string
}

DurableRegisterDecision is the core's response to an inbound durable register. Value-only (no js.Value) so it is host-testable.

func DecideDurableRegister

func DecideDurableRegister(key string, subscribed map[string]bool, incomingFields []string) DurableRegisterDecision

DecideDurableRegister is the pure subscribe/ack decision for a durable register.

An empty key is ignored (zero decision). Otherwise the core always announces the per-key online ack (so the (re)mounting component hydrates before it goes live), and subscribes to any incoming field it is not already subscribed to. `subscribed` is the core's set of (already-listening) field names for this key; a nil map means "nothing subscribed yet" (first register). The field names are carried through verbatim — this function never looks at any payload, only names, which is what keeps the core opaque.

type TopicRegisterDecision

type TopicRegisterDecision struct {
	// Subscribe is true on the FIRST registration of a key: the core must record
	// Fields and add a per-field req listener for each. Subsequent registrations
	// of the same key set Subscribe=false (the subscriptions already exist) but
	// still get an OnlineEvent so the late consumer is replayed + acked.
	Subscribe bool
	// Fields is the ordered field-name list to subscribe (only when Subscribe).
	// It is ROUTING metadata — the core stores/forwards each field's bytes
	// opaquely and never interprets them.
	Fields []string
	// OnlineEvent is the per-key online ack the core announces (empty only when
	// key is empty, which is ignored entirely).
	OnlineEvent string
}

TopicRegisterDecision is the core's response to an inbound topic registration. Value-only (no js.Value) so it is host-testable.

func DecideTopicRegister

func DecideTopicRegister(key string, alreadyKnown bool, incomingFields []string) TopicRegisterDecision

DecideTopicRegister is the pure subscribe/ack decision for a topic register.

An empty key is ignored (zero decision). Otherwise the core always announces the per-key online ack (so a late consumer hydrates), and subscribes to the key's per-field req events exactly once — on the first registration, keyed by alreadyKnown. The field-name list is carried through verbatim; this function never looks at any payload, only names, which is what keeps the core opaque.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL