Tags: overview, dx, event-stream, porcelain, non-normative
Lace’s durable truth is record bytes, record facts, Datalog policy, exchange plans, and interlace convergence. Applications should not have to drive raw protocol phases or inspect store internals to use that truth. This document summarizes the normal developer-facing surface above specs 010–060.
The main surface is an event-driven interlace runtime. Porcelain
helpers such as get, list, and
store are convenience APIs that construct and drive
interlaces; they are not direct store access.
The lower specs define the protocol machinery:
010 records
020 record facts and views
030 Datalog
040 exchange policy
050 interlace
060 ILTP
Those layers are explicit about hashes, validated facts, peer advertisement claims, query views, exchange plans, and transfer invariants. Developer surfaces compile to or operate through those layers. They do not replace them with remote commands, raw database access, WebSocket-specific objects, or application-level truth shortcuts.
A validated record can be presented as an immutable row. Its hash is
the row identity, and fields are columns. A projection, or table view,
chooses field names such as Group, App,
Name, TAI, Signed-By, and
Data-Length, then shows one row per record hash with those
fields as columns.
This table shape is a developer surface over validated record facts.
It does not make rows mutable, does not make
Group/App/Name unique, and does
not expose raw store iteration. Public APIs still use interlace-backed
porcelain and store machine boundaries.
Rules and selector DSLs define dynamic subsets of rows. Those subsets may depend on other rows, such as membership evidence, link targets, manifests, or current name records. Interlace converges the rows selected by both sides.
A runtime package exposes a small application model:
FilesystemLace::open(...)
IndexedDbLace::open(...)
Lace::connect_websocket(...)
start_open_interlace(left, right, policy_or_plan) -> OpenInterlace
bounded_interlace(left, right, policy_or_plan) -> BoundedInterlace
list(target, selector_or_coordinate, options) -> ListOutcome
get(target, selector_or_coordinate, options) -> GetOutcome
store(target, records, policy_or_options) -> StoreOutcome
for_each(target, selector_or_coordinate, options) -> Open observation streamExact method names may differ by language/runtime, but the ownership cuts are stable:
Lace is a record-set authority backed by a
runtime-owned record store machine. It owns validated record storage,
StoreRecordId wake state, and runtime lifecycle state.LaceHandle is a shareable participant capability. It is
not a raw store, transaction, socket, WebSocket, or already-running
protocol machine.OpenInterlace and BoundedInterlace are
runtime-owned drivers around a core interlace machine. They own runtime
IO, timers, waiting, event queues, cancellation, and transport
adapters.StoreRecordId assignment, policy-neutral record-fact
access, record loads, and typed-record stores. Public app APIs do not
expose raw record insertion, hash-existence checks, record counts, or
store iteration.Lace::connect_websocket(...) and similar transport
helpers create or wrap a typed peer stream capability over ILTP bytes.
They are not store-like objects and not remote command clients.
Starting an open interlace constructs an OpenInterlace
driver. It is not the async wait point by itself. Waiting or
await belongs to driving work: polling
next_event, running a bounded interlace to completion, or
using interlace-backed porcelain such as list,
get, store, and for_each. Manual
runtimes drive the same objects with explicit
try_next_event-style steps.
Applications observe interlace progress through record/lifecycle
event streams. Raw protocol facts, advertisements, side-machine phases,
store-machine vtables, and InterlaceStepOutcome values are
advanced/internal driver surfaces, not normal application events.
Conceptual public events are:
pub enum InterlaceEvent {
RecordSent { side: InterlaceSide, record_hash: RecordHash },
RecordSaved { side: InterlaceSide, record_hash: RecordHash, store_id: Option<StoreRecordId> },
RecordListed { side: InterlaceSide, record_hash: RecordHash },
FixedPoint,
Complete { outcome: BoundedInterlaceOutcome },
PeerClosed,
Closed,
LogicError { error: LogicError },
RuntimeIoError { error: RuntimeIoError },
RuntimeStoreError { error: RuntimeStoreError },
}A runtime may include record bytes in events when doing so is cheap
and well-owned, or may expose record hashes plus a follow-up record
result. The event must remain record/lifecycle-oriented. It must not
expose protocol fact blocks,
MaySend/MayRequest internals, peer
advertisement records, store cursors, raw NotAvailable
machinery, or low-level side-machine phases as normal app API.
InterlaceSide is an event-correlation label for this
interlace. It does not mean authority, source/sink direction, durable
receipt, initiator, locality, or transport.
try_next_event-style APIs perform at most one natural
core unit plus minimal runtime interpretation, then return an event or
no-event/would-block status. next_event-style APIs may
drive natural units until an event is available or a runtime wait point
is reached, then wait on runtime-owned IO, store readiness, timers, or
cancellation. Runtime wrappers must not hide an unbounded pump behind a
callback or invoke application observers while store locks or transport
callbacks are active.
After a terminal event (Complete, Closed,
PeerClosed, or fatal error), later polls report stream
exhaustion rather than rerunning closed work.
Event queues and transport buffers are bounded. If an application stops consuming events, or a peer stops accepting bytes, the runtime must backpressure, stop reading, cancel/close, or drop only explicitly diagnostic data according to a documented policy. It must not grow unbounded buffers or collapse record accounting events.
get, list, store, and
for_each are developer conveniences over interlace. They
are allowed record access paths because they construct operation laces
and run the same convergence machinery as explicit interlaces.
list(target, selector_or_coordinate, limit/options) is
a bounded one-shot operation against any LaceHandle. It
returns a finite result collection plus an operation status such as
complete, more-may-exist, stale/incomplete, cancelled, or failed. It may
use a trusted direct local list route when both the handle and policy
allow it; otherwise it uses ordinary interlace.get(target, selector_or_coordinate, options) is the
bounded one-result form over the same selection machinery and returns an
optional record/hash plus an operation status.store(target, records, policy/options) creates or uses
a source lace with typed validated records, runs a bounded provide/store
interlace to the target, and returns a store operation outcome rather
than an unbounded raw event buffer.for_each(target, selector_or_coordinate, options) is
the open/subscription porcelain. It runs an open interlace and invokes
or yields record-lifecycle observations for each newly matching record
until cancelled or closed.Porcelain result types report operation-level outcomes: complete, more-may-exist, stale/incomplete, policy-refused or invalid input records, duplicate/no-op saves, cancellation, and failure. The public event stream used while driving the operation remains record/lifecycle-only.
Porcelain must not mutate a target store directly, inspect raw target store contents, or bypass store-machine boundaries. Trusted/direct local routes are optimized interlace routes over store-machine boundaries, not raw store bypasses.
Coordinates (110) and higher-level selector DSLs help applications name common record sets: coordinate prefixes, latest versions, linked closures, signed records, or application surfaces. They are developer input languages. They compile to explicit Lace policy artifacts: canonical lacegrams, exchange-plan operands, local executable policies, query-exposure constraints, and list/get, store, or for_each configuration.
A coordinate lookup may use a runtime/store index internally, but selected truth still comes from validated record facts and policy evaluation. A coordinate is not a replacement for record hashes, lacegrams, exchange plans, or store-machine boundaries.
Faceted exchange plans, operand scopes, origin-aware fact resolution, and query views are hard to inspect. Developer tooling should provide diagnostics such as source maps, rule ids, traces, and route reports explaining:
MaySend(P) or
MayRequest(P);These diagnostics are separate from the normal record/lifecycle event stream. Canonical protocol text remains annotation-free. Authoring tools may keep source annotations and map them back to canonical policy, but those annotations are not protocol facts.
The developer-facing layer should be convenient without hiding Lace’s
ownership model: records are validated bytes, facts are explicit, query
views control peer visibility, interlace converges record sets, runtimes
own IO and waiting, and record store machines own persistence plus local
StoreRecordId assignment. Normal applications should use
interlace event streams and interlace-backed porcelain instead of raw
protocol or store APIs.